diff --git a/Makefile b/Makefile index d9739044e8..14ff5e5a32 100644 --- a/Makefile +++ b/Makefile @@ -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 @@ -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): @@ -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: @@ -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 diff --git a/cmd/lifecycle-server/main.go b/cmd/lifecycle-server/main.go new file mode 100644 index 0000000000..56584328e9 --- /dev/null +++ b/cmd/lifecycle-server/main.go @@ -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) + } +} diff --git a/cmd/lifecycle-server/start.go b/cmd/lifecycle-server/start.go new file mode 100644 index 0000000000..a24a56e2ad --- /dev/null +++ b/cmd/lifecycle-server/start.go @@ -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) + } + 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, + } + + 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 + } +} diff --git a/go.mod b/go.mod index 3aed0ff314..1550df17d2 100644 --- a/go.mod +++ b/go.mod @@ -13,12 +13,14 @@ 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 @@ -26,6 +28,7 @@ require ( 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 @@ -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 @@ -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 @@ -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 diff --git a/go.sum b/go.sum index 756de2111d..93cb217cea 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/operator-lifecycle-manager.Dockerfile b/operator-lifecycle-manager.Dockerfile index f1fe671ea4..9485fb0d71 100644 --- a/operator-lifecycle-manager.Dockerfile +++ b/operator-lifecycle-manager.Dockerfile @@ -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 diff --git a/pkg/lifecycle-server/fbc.go b/pkg/lifecycle-server/fbc.go new file mode 100644 index 0000000000..f53994926f --- /dev/null +++ b/pkg/lifecycle-server/fbc.go @@ -0,0 +1,122 @@ +/* +Copyright 2025. + +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 server + +import ( + "context" + "encoding/json" + "fmt" + "os" + "regexp" + "sync" + + "github.com/go-logr/logr" + "github.com/operator-framework/operator-registry/alpha/declcfg" + "k8s.io/apimachinery/pkg/util/sets" +) + +// versionPattern matches API versions like v1, v1alpha1, v2beta3 +// Matches: v1, v1alpha1, v1beta1, v200beta300 +// Does not match: 1, v0, v1beta0 +const versionPattern = `v[1-9][0-9]*(?:(?:alpha|beta)[1-9][0-9]*)?` + +// schemaVersionRegex matches lifecycle schema versions in FBC blobs +var schemaVersionRegex = regexp.MustCompile(`^io\.openshift\.operators\.lifecycles\.(` + versionPattern + `)$`) + +// LifecycleIndex maps schema version -> package name -> raw JSON blob +type LifecycleIndex map[string]map[string]json.RawMessage + +// LoadLifecycleData loads lifecycle blobs from FBC files at the given path +func LoadLifecycleData(fbcPath string, log logr.Logger) (LifecycleIndex, error) { + result := make(LifecycleIndex) + var mu sync.Mutex + + // Check if path exists + if _, err := os.Stat(fbcPath); os.IsNotExist(err) { + return result, nil + } + + root := os.DirFS(fbcPath) + err := declcfg.WalkMetasFS(context.Background(), root, func(path string, meta *declcfg.Meta, err error) error { + if err != nil { + log.Info("skipping FBC entry due to error", "path", path, "error", err) + return nil + } + if meta == nil { + return nil + } + + // Check if schema matches our pattern + matches := schemaVersionRegex.FindStringSubmatch(meta.Schema) + if matches == nil { + return nil + } + schemaVersion := matches[1] // e.g., "v1alpha1" + + if meta.Package == "" { + return nil + } + + // Store in index (thread-safe) + mu.Lock() + defer mu.Unlock() + if result[schemaVersion] == nil { + result[schemaVersion] = make(map[string]json.RawMessage) + } + if _, exists := result[schemaVersion][meta.Package]; exists { + return fmt.Errorf("duplicate lifecycle blob for version %q package %q in file %q", schemaVersion, meta.Package, path) + } + result[schemaVersion][meta.Package] = meta.Blob + + return nil + }) + + if err != nil { + return nil, err + } + + return result, nil +} + +// CountBlobs returns the total number of blobs in the index +func (index LifecycleIndex) CountBlobs() int { + count := 0 + for _, packages := range index { + count += len(packages) + } + return count +} + +// CountPackages returns the number of unique packages across all versions +func (index LifecycleIndex) CountPackages() int { + pkgs := sets.New[string]() + for _, packages := range index { + for pkg := range packages { + pkgs.Insert(pkg) + } + } + return pkgs.Len() +} + +// ListVersions returns the list of versions available in the index +func (index LifecycleIndex) ListVersions() []string { + versions := make([]string, 0, len(index)) + for v := range index { + versions = append(versions, v) + } + return versions +} diff --git a/pkg/lifecycle-server/fbc_test.go b/pkg/lifecycle-server/fbc_test.go new file mode 100644 index 0000000000..3db6bc69b8 --- /dev/null +++ b/pkg/lifecycle-server/fbc_test.go @@ -0,0 +1,461 @@ +package server + +import ( + "encoding/json" + "os" + "path/filepath" + "sort" + "testing" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/require" +) + +func TestSchemaVersionRegex(t *testing.T) { + tt := []struct { + name string + input string + matches bool + version string + }{ + { + name: "v1", + input: "io.openshift.operators.lifecycles.v1", + matches: true, + version: "v1", + }, + { + name: "v1alpha1", + input: "io.openshift.operators.lifecycles.v1alpha1", + matches: true, + version: "v1alpha1", + }, + { + name: "v1beta1", + input: "io.openshift.operators.lifecycles.v1beta1", + matches: true, + version: "v1beta1", + }, + { + name: "v2beta3", + input: "io.openshift.operators.lifecycles.v2beta3", + matches: true, + version: "v2beta3", + }, + { + name: "v200beta300", + input: "io.openshift.operators.lifecycles.v200beta300", + matches: true, + version: "v200beta300", + }, + { + name: "missing v prefix", + input: "io.openshift.operators.lifecycles.1", + matches: false, + }, + { + name: "v0 not allowed", + input: "io.openshift.operators.lifecycles.v0", + matches: false, + }, + { + name: "v1beta0 not allowed", + input: "io.openshift.operators.lifecycles.v1beta0", + matches: false, + }, + { + name: "v0alpha1 not allowed", + input: "io.openshift.operators.lifecycles.v0alpha1", + matches: false, + }, + { + name: "random schema", + input: "olm.package", + matches: false, + }, + { + name: "empty string", + input: "", + matches: false, + }, + { + name: "partial prefix match", + input: "io.openshift.operators.lifecycles.", + matches: false, + }, + { + name: "wrong prefix", + input: "io.openshift.operators.lifecycle.v1", + matches: false, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + matches := schemaVersionRegex.FindStringSubmatch(tc.input) + if tc.matches { + require.NotNil(t, matches, "expected %q to match", tc.input) + require.Equal(t, tc.version, matches[1], "extracted version mismatch for %q", tc.input) + } else { + require.Nil(t, matches, "expected %q not to match", tc.input) + } + }) + } +} + +// writeFBCFile writes a JSON file containing FBC meta objects to the given directory. +// Each object must be a map with "schema", "package", and other fields that become the blob. +func writeFBCFile(t *testing.T, dir, filename string, objects ...map[string]any) { + t.Helper() + var data []byte + for _, obj := range objects { + b, err := json.Marshal(obj) + require.NoError(t, err) + data = append(data, b...) + data = append(data, '\n') + } + err := os.WriteFile(filepath.Join(dir, filename), data, 0644) + require.NoError(t, err) +} + +func TestLoadLifecycleData(t *testing.T) { + tt := []struct { + name string + setup func(t *testing.T) string + expectedIndex LifecycleIndex + expectErr bool + }{ + { + name: "non-existent path returns empty index", + setup: func(t *testing.T) string { + return filepath.Join(t.TempDir(), "does-not-exist") + }, + expectedIndex: LifecycleIndex{}, + }, + { + name: "empty directory returns empty index", + setup: func(t *testing.T) string { + return t.TempDir() + }, + expectedIndex: LifecycleIndex{}, + }, + { + name: "lifecycle blob is indexed correctly", + setup: func(t *testing.T) string { + dir := t.TempDir() + writeFBCFile(t, dir, "catalog.json", + map[string]any{ + "schema": "io.openshift.operators.lifecycles.v1alpha1", + "package": "my-operator", + "data": "test-value", + }, + ) + return dir + }, + expectedIndex: LifecycleIndex{ + "v1alpha1": { + "my-operator": json.RawMessage(`{"data":"test-value","package":"my-operator","schema":"io.openshift.operators.lifecycles.v1alpha1"}`), + }, + }, + }, + { + name: "non-lifecycle schemas are skipped", + setup: func(t *testing.T) string { + dir := t.TempDir() + writeFBCFile(t, dir, "catalog.json", + map[string]any{ + "schema": "olm.package", + "package": "my-operator", + "name": "my-operator", + }, + map[string]any{ + "schema": "olm.channel", + "package": "my-operator", + "name": "stable", + }, + ) + return dir + }, + expectedIndex: LifecycleIndex{}, + }, + { + name: "multiple versions and packages", + setup: func(t *testing.T) string { + dir := t.TempDir() + writeFBCFile(t, dir, "catalog.json", + map[string]any{ + "schema": "io.openshift.operators.lifecycles.v1alpha1", + "package": "operator-a", + "status": "active", + }, + map[string]any{ + "schema": "io.openshift.operators.lifecycles.v1alpha1", + "package": "operator-b", + "status": "deprecated", + }, + map[string]any{ + "schema": "io.openshift.operators.lifecycles.v1", + "package": "operator-a", + "level": "ga", + }, + ) + return dir + }, + expectedIndex: LifecycleIndex{ + "v1alpha1": { + "operator-a": json.RawMessage(`{"package":"operator-a","schema":"io.openshift.operators.lifecycles.v1alpha1","status":"active"}`), + "operator-b": json.RawMessage(`{"package":"operator-b","schema":"io.openshift.operators.lifecycles.v1alpha1","status":"deprecated"}`), + }, + "v1": { + "operator-a": json.RawMessage(`{"level":"ga","package":"operator-a","schema":"io.openshift.operators.lifecycles.v1"}`), + }, + }, + }, + { + name: "empty package name is skipped", + setup: func(t *testing.T) string { + dir := t.TempDir() + writeFBCFile(t, dir, "catalog.json", + map[string]any{ + "schema": "io.openshift.operators.lifecycles.v1alpha1", + "data": "should-be-skipped", + }, + ) + return dir + }, + expectedIndex: LifecycleIndex{}, + }, + { + name: "mixed lifecycle and non-lifecycle schemas", + setup: func(t *testing.T) string { + dir := t.TempDir() + writeFBCFile(t, dir, "catalog.json", + map[string]any{ + "schema": "olm.package", + "package": "my-operator", + "name": "my-operator", + }, + map[string]any{ + "schema": "io.openshift.operators.lifecycles.v1alpha1", + "package": "my-operator", + "eol": "2025-12-31", + }, + ) + return dir + }, + expectedIndex: LifecycleIndex{ + "v1alpha1": { + "my-operator": json.RawMessage(`{"eol":"2025-12-31","package":"my-operator","schema":"io.openshift.operators.lifecycles.v1alpha1"}`), + }, + }, + }, + { + name: "corrupted entries are logged and skipped, valid entries still loaded", + setup: func(t *testing.T) string { + dir := t.TempDir() + // Write a valid lifecycle blob + writeFBCFile(t, dir, "valid.json", + map[string]any{ + "schema": "io.openshift.operators.lifecycles.v1alpha1", + "package": "good-operator", + "status": "active", + }, + ) + // Write a file with invalid JSON (corrupted entry) + err := os.WriteFile(filepath.Join(dir, "corrupted.json"), []byte("not valid json{{{"), 0644) + require.NoError(t, err) + return dir + }, + // WalkMetasFS passes per-meta errors to the callback, where LoadLifecycleData + // logs and skips them. No error is returned overall, and valid entries + // from other files are still loaded successfully. + expectedIndex: LifecycleIndex{ + "v1alpha1": { + "good-operator": json.RawMessage(`{"package":"good-operator","schema":"io.openshift.operators.lifecycles.v1alpha1","status":"active"}`), + }, + }, + }, + { + name: "duplicate lifecycle blob returns error", + setup: func(t *testing.T) string { + dir := t.TempDir() + writeFBCFile(t, dir, "catalog.json", + map[string]any{ + "schema": "io.openshift.operators.lifecycles.v1alpha1", + "package": "my-operator", + "status": "active", + }, + map[string]any{ + "schema": "io.openshift.operators.lifecycles.v1alpha1", + "package": "my-operator", + "status": "deprecated", + }, + ) + return dir + }, + expectErr: true, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + path := tc.setup(t) + result, err := LoadLifecycleData(path, logr.Discard()) + if tc.expectErr { + require.Error(t, err, "expected LoadLifecycleData to return an error") + return + } + require.NoError(t, err, "LoadLifecycleData returned unexpected error") + + // Compare version keys + require.Equal(t, len(tc.expectedIndex), len(result), "version count mismatch") + for version, expectedPkgs := range tc.expectedIndex { + resultPkgs, ok := result[version] + require.True(t, ok, "missing version %q in result", version) + require.Equal(t, len(expectedPkgs), len(resultPkgs), "package count mismatch for version %q", version) + for pkg, expectedBlob := range expectedPkgs { + resultBlob, ok := resultPkgs[pkg] + require.True(t, ok, "missing package %q in version %q", pkg, version) + // Compare as unmarshalled maps since JSON key order is not guaranteed + var expectedMap, resultMap map[string]any + require.NoError(t, json.Unmarshal(expectedBlob, &expectedMap), "failed to unmarshal expected blob for package %q", pkg) + require.NoError(t, json.Unmarshal(resultBlob, &resultMap), "failed to unmarshal result blob for package %q", pkg) + require.Equal(t, expectedMap, resultMap, "blob content mismatch for version %q package %q", version, pkg) + } + } + }) + } +} + +func TestLifecycleIndex_CountBlobs(t *testing.T) { + tt := []struct { + name string + index LifecycleIndex + expected int + }{ + { + name: "empty index", + index: LifecycleIndex{}, + expected: 0, + }, + { + name: "single version single package", + index: LifecycleIndex{ + "v1": {"pkg-a": json.RawMessage(`{}`)}, + }, + expected: 1, + }, + { + name: "multiple versions and packages", + index: LifecycleIndex{ + "v1alpha1": { + "pkg-a": json.RawMessage(`{}`), + "pkg-b": json.RawMessage(`{}`), + }, + "v1": { + "pkg-a": json.RawMessage(`{}`), + }, + }, + expected: 3, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expected, tc.index.CountBlobs(), "CountBlobs mismatch") + }) + } +} + +func TestLifecycleIndex_CountPackages(t *testing.T) { + tt := []struct { + name string + index LifecycleIndex + expected int + }{ + { + name: "empty index", + index: LifecycleIndex{}, + expected: 0, + }, + { + name: "same package across versions counted once", + index: LifecycleIndex{ + "v1alpha1": {"pkg-a": json.RawMessage(`{}`)}, + "v1": {"pkg-a": json.RawMessage(`{}`)}, + }, + expected: 1, + }, + { + name: "different packages counted separately", + index: LifecycleIndex{ + "v1alpha1": { + "pkg-a": json.RawMessage(`{}`), + "pkg-b": json.RawMessage(`{}`), + }, + "v1": { + "pkg-c": json.RawMessage(`{}`), + }, + }, + expected: 3, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expected, tc.index.CountPackages(), "CountPackages mismatch") + }) + } +} + +func TestLifecycleIndex_ListVersions(t *testing.T) { + tt := []struct { + name string + index LifecycleIndex + expected []string + }{ + { + name: "empty index", + index: LifecycleIndex{}, + expected: []string{}, + }, + { + name: "multiple versions", + index: LifecycleIndex{ + "v1alpha1": {"pkg-a": json.RawMessage(`{}`)}, + "v1": {"pkg-a": json.RawMessage(`{}`)}, + "v2beta1": {"pkg-b": json.RawMessage(`{}`)}, + }, + expected: []string{"v1", "v1alpha1", "v2beta1"}, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + result := tc.index.ListVersions() + sort.Strings(result) + sort.Strings(tc.expected) + require.Equal(t, tc.expected, result, "ListVersions mismatch") + }) + } +} + +func TestLoadLifecycleData_Subdirectory(t *testing.T) { + dir := t.TempDir() + subdir := filepath.Join(dir, "nested", "deep") + err := os.MkdirAll(subdir, 0755) + require.NoError(t, err, "failed to create nested test directory") + + writeFBCFile(t, subdir, "nested-catalog.json", + map[string]any{ + "schema": "io.openshift.operators.lifecycles.v1alpha1", + "package": "nested-operator", + "status": "active", + }, + ) + + result, err := LoadLifecycleData(dir, logr.Discard()) + require.NoError(t, err, "LoadLifecycleData failed on nested directory") + require.Contains(t, result, "v1alpha1", "expected v1alpha1 version in result from nested directory") + require.Contains(t, result["v1alpha1"], "nested-operator", "expected nested-operator package in v1alpha1") +} diff --git a/pkg/lifecycle-server/server.go b/pkg/lifecycle-server/server.go new file mode 100644 index 0000000000..10e6d2ad4d --- /dev/null +++ b/pkg/lifecycle-server/server.go @@ -0,0 +1,90 @@ +/* +Copyright 2025. + +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 server + +import ( + "net/http" + + "github.com/go-logr/logr" +) + +// NewHealthHandler creates an HTTP handler for health and readiness probes. +// The /healthz endpoint always returns 200. The /readyz endpoint returns 200 +// if lifecycle data is loaded, or 503 if the index is empty. +func NewHealthHandler(data LifecycleIndex) http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + }) + mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, r *http.Request) { + if len(data) == 0 { + w.WriteHeader(http.StatusServiceUnavailable) + _, _ = w.Write([]byte("no lifecycle data loaded")) + return + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + }) + return mux +} + +// NewHandler creates an HTTP handler for the lifecycle API. +// It serves GET /api/{version}/lifecycles/{package}, returning the raw JSON +// blob for the given version and package, 404 if not found, or 503 if no +// lifecycle data is loaded. +func NewHandler(data LifecycleIndex, log logr.Logger) http.Handler { + mux := http.NewServeMux() + + // GET /api/{version}/lifecycles/{package} + mux.HandleFunc("GET /api/{version}/lifecycles/{package}", func(w http.ResponseWriter, r *http.Request) { + version := r.PathValue("version") + pkg := r.PathValue("package") + + // If no lifecycle data is available, return 503 Service Unavailable + if len(data) == 0 { + log.V(1).Info("no lifecycle data available, returning 503") + http.Error(w, "No lifecycle data available", http.StatusServiceUnavailable) + return + } + + // Look up version in index + versionData, ok := data[version] + if !ok { + log.V(1).Info("version not found", "version", version, "package", pkg) + http.NotFound(w, r) + return + } + + // Look up package in version + rawJSON, ok := versionData[pkg] + if !ok { + log.V(1).Info("package not found", "version", version, "package", pkg) + http.NotFound(w, r) + return + } + + log.V(1).Info("returning lifecycle data", "version", version, "package", pkg) + + w.Header().Set("Content-Type", "application/json") + if _, err := w.Write(rawJSON); err != nil { + log.V(1).Error(err, "failed to write response") + } + }) + + return mux +} diff --git a/pkg/lifecycle-server/server_test.go b/pkg/lifecycle-server/server_test.go new file mode 100644 index 0000000000..41d032996b --- /dev/null +++ b/pkg/lifecycle-server/server_test.go @@ -0,0 +1,289 @@ +package server + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "sync" + "testing" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/require" +) + +func TestNewHandler(t *testing.T) { + testBlob := json.RawMessage(`{"eol":"2025-12-31","status":"active"}`) + + tt := []struct { + name string + data LifecycleIndex + method string + path string + expectedStatus int + expectedBody string + expectedCT string + }{ + { + name: "valid version and package returns 200 with JSON", + data: LifecycleIndex{ + "v1alpha1": { + "my-operator": testBlob, + }, + }, + method: http.MethodGet, + path: "/api/v1alpha1/lifecycles/my-operator", + expectedStatus: http.StatusOK, + expectedBody: `{"eol":"2025-12-31","status":"active"}`, + expectedCT: "application/json", + }, + { + name: "empty data returns 503", + data: LifecycleIndex{}, + method: http.MethodGet, + path: "/api/v1alpha1/lifecycles/my-operator", + expectedStatus: http.StatusServiceUnavailable, + }, + { + name: "unknown version returns 404", + data: LifecycleIndex{ + "v1alpha1": { + "my-operator": testBlob, + }, + }, + method: http.MethodGet, + path: "/api/v2/lifecycles/my-operator", + expectedStatus: http.StatusNotFound, + }, + { + name: "known version unknown package returns 404", + data: LifecycleIndex{ + "v1alpha1": { + "my-operator": testBlob, + }, + }, + method: http.MethodGet, + path: "/api/v1alpha1/lifecycles/other-operator", + expectedStatus: http.StatusNotFound, + }, + { + name: "POST method not allowed", + data: LifecycleIndex{ + "v1alpha1": { + "my-operator": testBlob, + }, + }, + method: http.MethodPost, + path: "/api/v1alpha1/lifecycles/my-operator", + expectedStatus: http.StatusMethodNotAllowed, + }, + { + name: "wrong path returns 404", + data: LifecycleIndex{ + "v1alpha1": { + "my-operator": testBlob, + }, + }, + method: http.MethodGet, + path: "/wrong/path", + expectedStatus: http.StatusNotFound, + }, + { + name: "nil data (nil map) returns 503", + data: nil, + method: http.MethodGet, + path: "/api/v1alpha1/lifecycles/my-operator", + expectedStatus: http.StatusServiceUnavailable, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + handler := NewHandler(tc.data, logr.Discard()) + + req := httptest.NewRequest(tc.method, tc.path, nil) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + resp := rec.Result() + defer resp.Body.Close() + require.Equal(t, tc.expectedStatus, resp.StatusCode, "unexpected status code") + + if tc.expectedBody != "" { + body, err := io.ReadAll(resp.Body) + require.NoError(t, err, "failed to read response body") + require.Equal(t, tc.expectedBody, string(body), "unexpected response body") + } + + if tc.expectedCT != "" { + require.Equal(t, tc.expectedCT, resp.Header.Get("Content-Type"), "unexpected Content-Type header") + } + }) + } +} + +func TestNewHandler_RawBlobReturnedByteForByte(t *testing.T) { + // Verify that the raw JSON blob is returned exactly as stored, not re-serialized. + // This matters because the handler writes rawJSON directly with w.Write(rawJSON). + originalBlob := json.RawMessage(`{"keys":"in-specific-order","numbers":42,"nested":{"a":1}}`) + + data := LifecycleIndex{ + "v1alpha1": { + "test-pkg": originalBlob, + }, + } + + handler := NewHandler(data, logr.Discard()) + req := httptest.NewRequest(http.MethodGet, "/api/v1alpha1/lifecycles/test-pkg", nil) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + resp := rec.Result() + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + require.NoError(t, err, "failed to read response body") + require.Equal(t, string(originalBlob), string(body), "response body should be byte-for-byte identical to the stored blob") +} + +func TestNewHandler_ConcurrentRequests(t *testing.T) { + testBlob := json.RawMessage(`{"status":"active","eol":"2025-12-31"}`) + data := LifecycleIndex{ + "v1alpha1": { + "my-operator": testBlob, + }, + } + handler := NewHandler(data, logr.Discard()) + + const goroutines = 50 + var wg sync.WaitGroup + wg.Add(goroutines) + + errCh := make(chan error, goroutines) + for range goroutines { + go func() { + defer wg.Done() + req := httptest.NewRequest(http.MethodGet, "/api/v1alpha1/lifecycles/my-operator", nil) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + resp := rec.Result() + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + errCh <- fmt.Errorf("expected status 200, got %d", resp.StatusCode) + return + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + errCh <- fmt.Errorf("failed to read body: %w", err) + return + } + if string(body) != string(testBlob) { + errCh <- fmt.Errorf("body mismatch: got %q, want %q", string(body), string(testBlob)) + } + }() + } + wg.Wait() + close(errCh) + + for err := range errCh { + t.Error(err) + } +} + +func TestNewHealthHandler(t *testing.T) { + tt := []struct { + name string + data LifecycleIndex + path string + expectedStatus int + expectedBody string + }{ + { + name: "healthz always returns 200", + data: LifecycleIndex{}, + path: "/healthz", + expectedStatus: http.StatusOK, + expectedBody: "ok", + }, + { + name: "healthz returns 200 with data", + data: LifecycleIndex{"v1": {"pkg": json.RawMessage(`{}`)}}, + path: "/healthz", + expectedStatus: http.StatusOK, + expectedBody: "ok", + }, + { + name: "readyz returns 503 when empty", + data: LifecycleIndex{}, + path: "/readyz", + expectedStatus: http.StatusServiceUnavailable, + expectedBody: "no lifecycle data loaded", + }, + { + name: "readyz returns 503 when nil", + data: nil, + path: "/readyz", + expectedStatus: http.StatusServiceUnavailable, + expectedBody: "no lifecycle data loaded", + }, + { + name: "readyz returns 200 when data loaded", + data: LifecycleIndex{"v1alpha1": {"my-operator": json.RawMessage(`{}`)}}, + path: "/readyz", + expectedStatus: http.StatusOK, + expectedBody: "ok", + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + handler := NewHealthHandler(tc.data) + req := httptest.NewRequest(http.MethodGet, tc.path, nil) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + resp := rec.Result() + defer resp.Body.Close() + require.Equal(t, tc.expectedStatus, resp.StatusCode, "unexpected status code") + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err, "failed to read response body") + require.Contains(t, string(body), tc.expectedBody, "unexpected response body") + }) + } +} + +func TestNewHandler_MultipleVersions(t *testing.T) { + blobV1Alpha1 := json.RawMessage(`{"version":"v1alpha1","status":"active"}`) + blobV1Beta1 := json.RawMessage(`{"version":"v1beta1","status":"deprecated"}`) + + data := LifecycleIndex{ + "v1alpha1": {"my-operator": blobV1Alpha1}, + "v1beta1": {"my-operator": blobV1Beta1}, + } + handler := NewHandler(data, logr.Discard()) + + // Query v1alpha1 + req := httptest.NewRequest(http.MethodGet, "/api/v1alpha1/lifecycles/my-operator", nil) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + resp := rec.Result() + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode, "v1alpha1 request should return 200") + body, err := io.ReadAll(resp.Body) + require.NoError(t, err, "failed to read v1alpha1 response body") + require.Equal(t, string(blobV1Alpha1), string(body), "v1alpha1 response body mismatch") + + // Query v1beta1 + req2 := httptest.NewRequest(http.MethodGet, "/api/v1beta1/lifecycles/my-operator", nil) + rec2 := httptest.NewRecorder() + handler.ServeHTTP(rec2, req2) + resp2 := rec2.Result() + defer resp2.Body.Close() + require.Equal(t, http.StatusOK, resp2.StatusCode, "v1beta1 request should return 200") + body2, err := io.ReadAll(resp2.Body) + require.NoError(t, err, "failed to read v1beta1 response body") + require.Equal(t, string(blobV1Beta1), string(body2), "v1beta1 response body mismatch") +} diff --git a/vendor/github.com/openshift/library-go/pkg/crypto/crypto.go b/vendor/github.com/openshift/library-go/pkg/crypto/crypto.go index 696278eaf0..ca2806ecc6 100644 --- a/vendor/github.com/openshift/library-go/pkg/crypto/crypto.go +++ b/vendor/github.com/openshift/library-go/pkg/crypto/crypto.go @@ -159,17 +159,9 @@ var openSSLToIANACiphersMap = map[string]string{ "ECDHE-RSA-CHACHA20-POLY1305": "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", // 0xCC,0xA8 "ECDHE-ECDSA-AES128-SHA256": "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", // 0xC0,0x23 "ECDHE-RSA-AES128-SHA256": "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", // 0xC0,0x27 - "ECDHE-ECDSA-AES256-SHA384": "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", // 0xC0,0x24 - "ECDHE-RSA-AES256-SHA384": "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", // 0xC0,0x28 - "DHE-RSA-AES128-GCM-SHA256": "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", // 0x00,0x9E - "DHE-RSA-AES256-GCM-SHA384": "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", // 0x00,0x9F - "DHE-RSA-CHACHA20-POLY1305": "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256", // 0xCC,0xAA - "DHE-RSA-AES128-SHA256": "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", // 0x00,0x67 - "DHE-RSA-AES256-SHA256": "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", // 0x00,0x6B "AES128-GCM-SHA256": "TLS_RSA_WITH_AES_128_GCM_SHA256", // 0x00,0x9C "AES256-GCM-SHA384": "TLS_RSA_WITH_AES_256_GCM_SHA384", // 0x00,0x9D "AES128-SHA256": "TLS_RSA_WITH_AES_128_CBC_SHA256", // 0x00,0x3C - "AES256-SHA256": "TLS_RSA_WITH_AES_256_CBC_SHA256", // 0x00,0x3D // TLS 1 "ECDHE-ECDSA-AES128-SHA": "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", // 0xC0,0x09 @@ -178,10 +170,9 @@ var openSSLToIANACiphersMap = map[string]string{ "ECDHE-RSA-AES256-SHA": "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", // 0xC0,0x14 // SSL 3 - "AES128-SHA": "TLS_RSA_WITH_AES_128_CBC_SHA", // 0x00,0x2F - "AES256-SHA": "TLS_RSA_WITH_AES_256_CBC_SHA", // 0x00,0x35 - "DES-CBC3-SHA": "TLS_RSA_WITH_3DES_EDE_CBC_SHA", // 0x00,0x0A - "ECDHE-RSA-DES-CBC3-SHA": "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", // 0xC0,0x12 + "AES128-SHA": "TLS_RSA_WITH_AES_128_CBC_SHA", // 0x00,0x2F + "AES256-SHA": "TLS_RSA_WITH_AES_256_CBC_SHA", // 0x00,0x35 + "DES-CBC3-SHA": "TLS_RSA_WITH_3DES_EDE_CBC_SHA", // 0x00,0x0A } // CipherSuitesToNamesOrDie given a list of cipher suites as ints, return their readable names diff --git a/vendor/modules.txt b/vendor/modules.txt index 42da3c226b..8879fba4e9 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -635,7 +635,7 @@ github.com/openshift/client-go/config/informers/externalversions/internalinterfa github.com/openshift/client-go/config/listers/config/v1 github.com/openshift/client-go/config/listers/config/v1alpha1 github.com/openshift/client-go/config/listers/config/v1alpha2 -# github.com/openshift/library-go v0.0.0-20260204111611-b7d4fa0e292a +# github.com/openshift/library-go v0.0.0-20260205095356-7bced6e899b6 ## explicit; go 1.24.0 github.com/openshift/library-go/pkg/crypto # github.com/operator-framework/api v0.42.0 => ./staging/api