Skip to content
Open
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ COLLECT_PROFILES_CMD := $(addprefix bin/, collect-profiles)
OPM := $(addprefix bin/, opm)
OLM_CMDS := $(shell go list -mod=vendor $(OLM_PKG)/cmd/...)
PSM_CMD := $(addprefix bin/, psm)
LIFECYCLE_SERVER_CMD := $(addprefix bin/, lifecycle-server)
REGISTRY_CMDS := $(addprefix bin/, $(shell ls staging/operator-registry/cmd | grep -v opm))

# Default image tag for build/olm-container and build/registry-container
Expand Down Expand Up @@ -77,7 +78,7 @@ build/registry:
$(MAKE) $(REGISTRY_CMDS) $(OPM)

build/olm:
$(MAKE) $(PSM_CMD) $(OLM_CMDS) $(COLLECT_PROFILES_CMD) bin/copy-content
$(MAKE) $(PSM_CMD) $(OLM_CMDS) $(COLLECT_PROFILES_CMD) bin/copy-content $(LIFECYCLE_SERVER_CMD)

$(OPM): version_flags=-ldflags "-X '$(REGISTRY_PKG)/cmd/opm/version.gitCommit=$(GIT_COMMIT)' -X '$(REGISTRY_PKG)/cmd/opm/version.opmVersion=$(OPM_VERSION)' -X '$(REGISTRY_PKG)/cmd/opm/version.buildDate=$(BUILD_DATE)'"
$(OPM):
Expand All @@ -97,6 +98,9 @@ $(PSM_CMD): FORCE
$(COLLECT_PROFILES_CMD): FORCE
go build $(GO_BUILD_OPTS) $(GO_BUILD_TAGS) -o $(COLLECT_PROFILES_CMD) $(ROOT_PKG)/cmd/collect-profiles

$(LIFECYCLE_SERVER_CMD): FORCE
go build $(GO_BUILD_OPTS) $(GO_BUILD_TAGS) -o $(LIFECYCLE_SERVER_CMD) $(ROOT_PKG)/cmd/lifecycle-server

.PHONY: cross
cross: version_flags=-X '$(REGISTRY_PKG)/cmd/opm/version.gitCommit=$(GIT_COMMIT)' -X '$(REGISTRY_PKG)/cmd/opm/version.opmVersion=$(OPM_VERSION)' -X '$(REGISTRY_PKG)/cmd/opm/version.buildDate=$(BUILD_DATE)'
cross:
Expand Down Expand Up @@ -133,6 +137,9 @@ unit/api:
unit/psm:
go test $(ROOT_DIR)/pkg/package-server-manager/...

unit/lifecycle-server:
go test $(ROOT_DIR)/pkg/lifecycle-server/...

unit: ## Run unit tests
$(ROOT_DIR)/scripts/unit.sh

Expand Down
22 changes: 22 additions & 0 deletions cmd/lifecycle-server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package main

import (
"fmt"
"os"

"github.com/spf13/cobra"
)

func main() {
rootCmd := &cobra.Command{
Use: "lifecycle-server",
Short: "Lifecycle Metadata Server for OLM",
}

rootCmd.AddCommand(newStartCmd())

if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "error running lifecycle-server: %v\n", err)
os.Exit(1)
}
}
244 changes: 244 additions & 0 deletions cmd/lifecycle-server/start.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package main

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

"github.com/openshift/library-go/pkg/crypto"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"

"k8s.io/klog/v2"

server "github.com/openshift/operator-framework-olm/pkg/lifecycle-server"
)

const (
defaultFBCPath = "/catalog/configs"
defaultListenAddr = ":8443"
defaultHealthAddr = ":8081"
defaultTLSCertPath = "/var/run/secrets/serving-cert/tls.crt"
defaultTLSKeyPath = "/var/run/secrets/serving-cert/tls.key"
shutdownTimeout = 10 * time.Second
readHeaderTimeout = 5 * time.Second
readTimeout = 10 * time.Second
writeTimeout = 30 * time.Second
idleTimeout = 120 * time.Second
)

var (
fbcPath string
listenAddr string
healthAddr string
tlsCertPath string
tlsKeyPath string
tlsMinVersionStr string
tlsCipherSuiteStrs []string
)

// newStartCmd creates the "start" subcommand with all CLI flags.
func newStartCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "start",
Short: "Start the Lifecycle Server",
SilenceUsage: true,
RunE: run,
}

cmd.Flags().StringVar(&fbcPath, "fbc-path", defaultFBCPath, "path to FBC catalog data")
cmd.Flags().StringVar(&listenAddr, "listen", defaultListenAddr, "address to listen on for HTTPS API")
cmd.Flags().StringVar(&healthAddr, "health", defaultHealthAddr, "address to listen on for health checks")
cmd.Flags().StringVar(&tlsCertPath, "tls-cert", defaultTLSCertPath, "path to TLS certificate")
cmd.Flags().StringVar(&tlsKeyPath, "tls-key", defaultTLSKeyPath, "path to TLS private key")
cmd.Flags().StringVar(&tlsMinVersionStr, "tls-min-version", "", "minimum TLS version")
cmd.Flags().StringSliceVar(&tlsCipherSuiteStrs, "tls-cipher-suites", nil, "comma-separated list of cipher suites")

return cmd
}

// parseTLSFlags builds a tls.Config from the provided cert/key paths, minimum
// version, and cipher suite names. The returned config uses GetCertificate to
// reload the keypair on each handshake, supporting certificate rotation.
func parseTLSFlags(certPath, keyPath, minVersionStr string, cipherSuiteStrs []string) (*tls.Config, error) {
// Using a function to load the keypair each time means that we automatically pick up the new certificate when it reloads.
getCertificate := func(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, err
}
return &cert, nil
}
if _, err := getCertificate(nil); err != nil {
return nil, fmt.Errorf("unable to load TLS certificate: %v", err)
}

minVersion, err := crypto.TLSVersion(minVersionStr)
if err != nil {
return nil, fmt.Errorf("invalid TLS minimum version: %s", minVersionStr)
}

var (
cipherSuites []uint16
cipherSuiteErrs []error
)
for _, tlsCipherSuiteStr := range cipherSuiteStrs {
tlsCipherSuite, err := crypto.CipherSuite(tlsCipherSuiteStr)
if err != nil {
cipherSuiteErrs = append(cipherSuiteErrs, err)
} else {
cipherSuites = append(cipherSuites, tlsCipherSuite)
}
}
if len(cipherSuiteErrs) != 0 {
return nil, fmt.Errorf("invalid TLS cipher suites: %v", errors.Join(cipherSuiteErrs...))
}

return &tls.Config{
GetCertificate: getCertificate,
MinVersion: minVersion,
CipherSuites: cipherSuites,
}, nil
}

// run is the main entrypoint for the "start" command. It loads FBC data,
// sets up authn/authz, and starts the API and health servers.
func run(_ *cobra.Command, _ []string) error {
log := klog.NewKlogr()
log.Info("starting lifecycle-server")

tlsConfig, err := parseTLSFlags(tlsCertPath, tlsKeyPath, tlsMinVersionStr, tlsCipherSuiteStrs)
if err != nil {
return fmt.Errorf("failed to parse tls flags: %w", err)
}

// Create Kubernetes client for authn/authz
restCfg := ctrl.GetConfigOrDie()
httpClient, err := rest.HTTPClientFor(restCfg)
if err != nil {
log.Error(err, "failed to create http client")
return err
}

authnzFilter, err := filters.WithAuthenticationAndAuthorization(restCfg, httpClient)
if err != nil {
log.Error(err, "failed to create authorization filter")
return err
}

// Load lifecycle data from FBC
log.Info("loading lifecycle data from FBC", "path", fbcPath)
data, err := server.LoadLifecycleData(fbcPath, log)
if err != nil {
return fmt.Errorf("failed to load lifecycle data: %w", err)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
log.Info("loaded lifecycle data",
"packageCount", data.CountPackages(),
"blobCount", data.CountBlobs(),
"versions", data.ListVersions(),
)

// Create HTTP apiHandler with authn/authz middleware
baseHandler := server.NewHandler(data, log)
apiHandler, err := authnzFilter(log, baseHandler)
if err != nil {
log.Error(err, "failed to create api handler")
return err
}

// Create health handler (no auth required)
healthHandler := server.NewHealthHandler(data)

// Create servers
apiServer := cancelableServer{
Server: &http.Server{
Addr: listenAddr,
Handler: apiHandler,
TLSConfig: tlsConfig,
ReadHeaderTimeout: readHeaderTimeout,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
IdleTimeout: idleTimeout,
},
ShutdownTimeout: shutdownTimeout,
}
healthServer := cancelableServer{
Server: &http.Server{
Addr: healthAddr,
Handler: healthHandler,
ReadHeaderTimeout: readHeaderTimeout,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
IdleTimeout: idleTimeout,
},
ShutdownTimeout: shutdownTimeout,
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

eg, ctx := errgroup.WithContext(ctrl.SetupSignalHandler())
eg.Go(func() error {
if err := apiServer.ListenAndServeTLS(ctx, "", ""); err != nil {
return fmt.Errorf("api server error: %w", err)
}
return nil
})
eg.Go(func() error {
if err := healthServer.ListenAndServe(ctx); err != nil {
return fmt.Errorf("health server error: %w", err)
}
return nil
})
return eg.Wait()
}

// cancelableServer wraps http.Server with context-aware listen methods
// that initiate graceful shutdown when the context is cancelled.
type cancelableServer struct {
*http.Server
ShutdownTimeout time.Duration
}

// ListenAndServe starts the server and shuts it down when ctx is cancelled.
func (s *cancelableServer) ListenAndServe(ctx context.Context) error {
return s.listenAndServe(ctx,
func() error {
return s.Server.ListenAndServe()
},
s.Server.Shutdown,
)
}
// ListenAndServeTLS starts the TLS server and shuts it down when ctx is cancelled.
func (s *cancelableServer) ListenAndServeTLS(ctx context.Context, certFile, keyFile string) error {
return s.listenAndServe(ctx,
func() error {
return s.Server.ListenAndServeTLS(certFile, keyFile)
},
s.Server.Shutdown,
)
}

// listenAndServe runs the server via runFunc and waits for either a server
// error or context cancellation, calling cancelFunc for graceful shutdown.
func (s *cancelableServer) listenAndServe(ctx context.Context, runFunc func() error, cancelFunc func(context.Context) error) error {
errChan := make(chan error, 1)
go func() {
errChan <- runFunc()
}()

select {
case err := <-errChan:
return err
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), s.ShutdownTimeout)
defer cancel()
if err := cancelFunc(shutdownCtx); err != nil {
return err
}
return nil
}
}
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,22 @@ require (
github.com/mikefarah/yq/v3 v3.0.0-20201202084205-8846255d1c37
github.com/onsi/ginkgo/v2 v2.28.2
github.com/openshift/api v0.0.0-20260204104751-e09e5a4ebcd0
github.com/openshift/library-go v0.0.0-20260205095356-7bced6e899b6
github.com/operator-framework/api v0.42.0
github.com/operator-framework/operator-lifecycle-manager v0.0.0-00010101000000-000000000000
github.com/operator-framework/operator-registry v1.66.0
github.com/sirupsen/logrus v1.9.4
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
golang.org/x/sync v0.20.0
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.1
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.35.4
k8s.io/apimachinery v0.35.4
k8s.io/client-go v0.35.4
k8s.io/code-generator v0.35.4
k8s.io/klog/v2 v2.140.0
k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4
k8s.io/utils v0.0.0-20260108192941-914a6e750570
sigs.k8s.io/controller-runtime v0.23.3
Expand Down Expand Up @@ -157,7 +160,6 @@ require (
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opencontainers/runtime-spec v1.3.0 // indirect
github.com/openshift/client-go v0.0.0-20260108185524-48f4ccfc4e13 // indirect
github.com/openshift/library-go v0.0.0-20260204111611-b7d4fa0e292a // indirect
github.com/otiai10/copy v1.14.1 // indirect
github.com/otiai10/mint v1.6.3 // indirect
github.com/pkg/errors v0.9.1 // indirect
Expand Down Expand Up @@ -209,7 +211,6 @@ require (
golang.org/x/mod v0.35.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/term v0.42.0 // indirect
golang.org/x/text v0.36.0 // indirect
Expand All @@ -231,7 +232,6 @@ require (
k8s.io/cli-runtime v0.35.0 // indirect
k8s.io/component-base v0.35.4 // indirect
k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b // indirect
k8s.io/klog/v2 v2.140.0 // indirect
k8s.io/kms v0.35.4 // indirect
k8s.io/kube-aggregator v0.35.4 // indirect
k8s.io/kubectl v0.35.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -429,8 +429,8 @@ github.com/openshift/api v0.0.0-20260204104751-e09e5a4ebcd0 h1:mj1uTiMB24CUakpEc
github.com/openshift/api v0.0.0-20260204104751-e09e5a4ebcd0/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY=
github.com/openshift/client-go v0.0.0-20260108185524-48f4ccfc4e13 h1:6rd4zSo2UaWQcAPZfHK9yzKVqH0BnMv1hqMzqXZyTds=
github.com/openshift/client-go v0.0.0-20260108185524-48f4ccfc4e13/go.mod h1:YvOmPmV7wcJxpfhTDuFqqs2Xpb3M3ovsM6Qs/i2ptq4=
github.com/openshift/library-go v0.0.0-20260204111611-b7d4fa0e292a h1:YLnZtVfqGUfTbQ+M06QAslEmP4WrnRoPrk4AtoBJdm8=
github.com/openshift/library-go v0.0.0-20260204111611-b7d4fa0e292a/go.mod h1:DCRz1EgdayEmr9b6KXKDL+DWBN0rGHu/VYADeHzPoOk=
github.com/openshift/library-go v0.0.0-20260205095356-7bced6e899b6 h1:YoT3Q+9/I3QMicrayX7ZwGZh8BFVKjaVat2gdMd8Ads=
github.com/openshift/library-go v0.0.0-20260205095356-7bced6e899b6/go.mod h1:DCRz1EgdayEmr9b6KXKDL+DWBN0rGHu/VYADeHzPoOk=
github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
Expand Down
1 change: 1 addition & 0 deletions operator-lifecycle-manager.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ COPY --from=builder /build/bin/cpb /bin/cpb
COPY --from=builder /build/bin/psm /bin/psm
COPY --from=builder /build/bin/copy-content /bin/copy-content
COPY --from=builder /tmp/build/olmv0-tests-ext.gz /usr/bin/olmv0-tests-ext.gz
COPY --from=builder /build/bin/lifecycle-server /bin/lifecycle-server

# This image doesn't need to run as root user.
USER 1001
Expand Down
Loading
You are viewing a condensed version of this merge commit. You can view the full changes here.