diff --git a/catalogd/cmd/catalogd/main.go b/catalogd/cmd/catalogd/main.go index 77698444c..bb91358f0 100644 --- a/catalogd/cmd/catalogd/main.go +++ b/catalogd/cmd/catalogd/main.go @@ -279,7 +279,7 @@ func main() { }, } - var localStorage storage.Instance + var store storage.Instance metrics.Registry.MustRegister(catalogdmetrics.RequestDurationMetric) storeDir := filepath.Join(cacheDir, storageDir) @@ -294,7 +294,11 @@ func main() { os.Exit(1) } - localStorage = storage.LocalDirV1{RootDir: storeDir, RootURL: baseStorageURL} + store, err = storage.NewSQLiteV1(storeDir, baseStorageURL) + if err != nil { + setupLog.Error(err, "unable to create storage instance") + os.Exit(1) + } // Config for the the catalogd web server catalogServerConfig := serverutil.CatalogServerConfig{ @@ -302,7 +306,7 @@ func main() { CatalogAddr: catalogServerAddr, CertFile: certFile, KeyFile: keyFile, - LocalStorage: localStorage, + LocalStorage: store, } err = serverutil.AddCatalogServerToManager(mgr, catalogServerConfig, cw) @@ -314,7 +318,7 @@ func main() { if err = (&corecontrollers.ClusterCatalogReconciler{ Client: mgr.GetClient(), Unpacker: unpacker, - Storage: localStorage, + Storage: store, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "ClusterCatalog") os.Exit(1) diff --git a/catalogd/internal/storage/sqlite.go b/catalogd/internal/storage/sqlite.go new file mode 100644 index 000000000..6052bc5f2 --- /dev/null +++ b/catalogd/internal/storage/sqlite.go @@ -0,0 +1,312 @@ +package storage + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "io/fs" + "net/http" + "net/url" + "os" + "path/filepath" + "sync" + + "github.com/google/uuid" + "github.com/operator-framework/operator-registry/alpha/declcfg" + _ "modernc.org/sqlite" +) + +type SQLiteV1 struct { + db *sql.DB + RootDir string + RootURL *url.URL + mu sync.RWMutex +} + +func NewSQLiteV1(rootDir string, rootURL *url.URL) (*SQLiteV1, error) { + if err := os.MkdirAll(rootDir, 0700); err != nil { + return nil, fmt.Errorf("failed to create root directory: %w", err) + } + + dbPath := filepath.Join(rootDir, "storage.db") + db, err := sql.Open("sqlite", dbPath+"?_pragma=journal_mode(WAL)&_pragma=synchronous(NORMAL)") + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } + + if err := initializeDB(db); err != nil { + db.Close() + return nil, err + } + + return &SQLiteV1{ + db: db, + RootDir: rootDir, + RootURL: rootURL, + }, nil +} + +func initializeDB(db *sql.DB) error { + _, err := db.Exec(` + CREATE TABLE IF NOT EXISTS metas ( + id TEXT PRIMARY KEY, + catalog_name TEXT NOT NULL, + schema TEXT NOT NULL, + package TEXT, + name TEXT NOT NULL, + blob TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + -- Index for catalog lookups + CREATE INDEX IF NOT EXISTS idx_metas_catalog + ON metas(catalog_name); + `) + return err +} + +func (s *SQLiteV1) Store(ctx context.Context, catalog string, fsys fs.FS) error { + s.mu.Lock() + defer s.mu.Unlock() + + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return fmt.Errorf("failed to begin transaction: %w", err) + } + defer tx.Rollback() + + // Delete existing entries for this catalog + if _, err := tx.ExecContext(ctx, "DELETE FROM metas WHERE catalog_name = ?", catalog); err != nil { + return fmt.Errorf("failed to delete existing catalog entries: %w", err) + } + + stmt, err := tx.PrepareContext(ctx, ` + INSERT INTO metas (id, catalog_name, schema, package, name, blob) + VALUES (?, ?, ?, ?, ?, ?) + `) + if err != nil { + return fmt.Errorf("failed to prepare statement: %w", err) + } + defer stmt.Close() + + err = declcfg.WalkMetasFS(ctx, fsys, func(path string, meta *declcfg.Meta, err error) error { + if err != nil { + return err + } + + // Generate a new UUID for each meta entry + id := uuid.New().String() + + // Handle empty package as NULL + // since schema=olm.package blobs don't have a `package` + // value and the value is instead in `name` + var pkgValue interface{} + if meta.Package != "" { + pkgValue = meta.Package + } + + _, err = stmt.ExecContext(ctx, + id, + catalog, + meta.Schema, + pkgValue, + meta.Name, + string(meta.Blob), + ) + if err != nil { + return fmt.Errorf("failed to insert meta: %w", err) + } + return nil + }) + if err != nil { + return fmt.Errorf("error walking FBC root: %w", err) + } + + return tx.Commit() +} + +func (s *SQLiteV1) Delete(catalog string) error { + s.mu.Lock() + defer s.mu.Unlock() + + _, err := s.db.Exec("DELETE FROM metas WHERE catalog_name = ?", catalog) + if err != nil { + return fmt.Errorf("failed to delete catalog: %w", err) + } + return nil +} + +func (s *SQLiteV1) BaseURL(catalog string) string { + return s.RootURL.JoinPath(catalog).String() +} + +func (s *SQLiteV1) handleV1All(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + catalog := r.PathValue("catalog") + if catalog == "" { + http.Error(w, "Not found", http.StatusNotFound) + return + } + + s.mu.RLock() + defer s.mu.RUnlock() + + rows, err := s.db.Query(` + SELECT schema, package, name, blob + FROM metas + WHERE catalog_name = ? + ORDER BY schema, + CASE WHEN package IS NULL THEN 1 ELSE 0 END, + package, + name + `, catalog) + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + defer rows.Close() + + w.Header().Set("Content-Type", "application/jsonl") + s.writeRows(w, rows) +} + +func (s *SQLiteV1) handleV1Query(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + s.mu.RLock() + defer s.mu.RUnlock() + + catalog := r.PathValue("catalog") + if catalog == "" { + http.Error(w, "Not found", http.StatusNotFound) + return + } + + // Get query parameters + schema := r.URL.Query().Get("schema") + pkg := r.URL.Query().Get("package") + name := r.URL.Query().Get("name") + + // If no parameters are provided, return entire catalog + if schema == "" && pkg == "" && name == "" { + rows, err := s.db.Query(` + SELECT schema, package, name, blob + FROM metas + WHERE catalog_name = ? + ORDER BY schema, + CASE WHEN package IS NULL THEN 1 ELSE 0 END, + package, + name + `, catalog) + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + defer rows.Close() + + w.Header().Set("Content-Type", "application/jsonl") + s.writeRows(w, rows) + return + } + + // Build query with provided parameters + query := ` + SELECT schema, package, name, blob + FROM metas + WHERE catalog_name = ? + ` + args := []interface{}{catalog} + + if schema != "" { + query += " AND schema = ?" + args = append(args, schema) + } + if pkg != "" { + query += " AND package = ?" + args = append(args, pkg) + } + if name != "" { + query += " AND name = ?" + args = append(args, name) + } + + query += ` + ORDER BY schema, + CASE WHEN package IS NULL THEN 1 ELSE 0 END, + package, + name + ` + + rows, err := s.db.Query(query, args...) + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + defer rows.Close() + + w.Header().Set("Content-Type", "application/jsonl") + s.writeRows(w, rows) +} + +// Helper function to write rows as JSON lines +func (s *SQLiteV1) writeRows(w http.ResponseWriter, rows *sql.Rows) { + encoder := json.NewEncoder(w) + + for rows.Next() { + var meta declcfg.Meta + var packageVal sql.NullString + var blobStr string + + if err := rows.Scan(&meta.Schema, &packageVal, &meta.Name, &blobStr); err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + if packageVal.Valid { + meta.Package = packageVal.String + } + + meta.Blob = json.RawMessage(blobStr) + + if err := encoder.Encode(meta); err != nil { + return + } + } +} + +func (s *SQLiteV1) StorageServerHandler() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc(s.RootURL.JoinPath("{catalog}", "api", "v1", "all").Path, s.handleV1All) + mux.HandleFunc(s.RootURL.JoinPath("{catalog}", "api", "v1", "query").Path, s.handleV1Query) + return mux +} + +func (s *SQLiteV1) ContentExists(catalog string) bool { + s.mu.RLock() + defer s.mu.RUnlock() + + var exists bool + err := s.db.QueryRow(` + SELECT EXISTS( + SELECT 1 FROM metas WHERE catalog_name = ? LIMIT 1 + ) + `, catalog).Scan(&exists) + if err != nil { + return false + } + return exists +} + +func (s *SQLiteV1) Close() error { + s.mu.Lock() + defer s.mu.Unlock() + return s.db.Close() +} diff --git a/catalogd/internal/storage/sqlite_test.go b/catalogd/internal/storage/sqlite_test.go new file mode 100644 index 000000000..07b853be5 --- /dev/null +++ b/catalogd/internal/storage/sqlite_test.go @@ -0,0 +1,391 @@ +package storage + +import ( + "context" + "fmt" + "io" + "io/fs" + "net/http" + "net/http/httptest" + "net/url" + "os" + "strings" + "sync" + "testing" + "testing/fstest" + + "github.com/stretchr/testify/require" +) + +func TestSqliteServerHandler(t *testing.T) { + store, err := NewSQLiteV1(t.TempDir(), &url.URL{Path: urlPrefix}) + require.NoError(t, err) + + testFS := fstest.MapFS{ + "meta.json": &fstest.MapFile{ + Data: []byte(`{"foo":"bar"}`), + }, + } + if store.Store(context.Background(), "test-catalog", testFS) != nil { + t.Fatal("failed to store test catalog and start server") + } + testServer := httptest.NewServer(store.StorageServerHandler()) + defer testServer.Close() + + for _, tc := range []struct { + name string + URLPath string + expectedStatusCode int + expectedContent string + }{ + { + name: "Server returns 404 when root URL is queried", + expectedStatusCode: http.StatusNotFound, + expectedContent: "404 page not found", + URLPath: "", + }, + { + name: "Server returns 404 when path '/' is queried", + expectedStatusCode: http.StatusNotFound, + expectedContent: "404 page not found", + URLPath: "/", + }, + { + name: "Server returns 404 when path '/catalogs/' is queried", + expectedStatusCode: http.StatusNotFound, + expectedContent: "404 page not found", + URLPath: "/catalogs/", + }, + { + name: "Server returns 404 when path '/catalogs//' is queried", + expectedStatusCode: http.StatusNotFound, + expectedContent: "404 page not found", + URLPath: "/catalogs/test-catalog/", + }, + { + name: "Server returns 404 when path '/catalogs//api/' is queried", + expectedStatusCode: http.StatusNotFound, + expectedContent: "404 page not found", + URLPath: "/catalogs/test-catalog/api/", + }, + { + name: "Serer return 404 when path '/catalogs//api/v1' is queried", + expectedStatusCode: http.StatusNotFound, + expectedContent: "404 page not found", + URLPath: "/catalogs/test-catalog/api/v1c", + }, + { + name: "Server return 404 when path '/catalogs//non-existent.txt' is queried", + expectedStatusCode: http.StatusNotFound, + expectedContent: "404 page not found", + URLPath: "/catalogs/test-catalog/non-existent.txt", + }, + { + name: "Server returns 404 when path '/catalogs/.jsonl' is queried even if the file exists, since we don't serve the filesystem, and serve an API instead", + expectedStatusCode: http.StatusNotFound, + expectedContent: "404 page not found", + URLPath: "/catalogs/test-catalog.jsonl", + }, + // { + // name: "Server returns 404 when non-existent catalog is queried", + // expectedStatusCode: http.StatusNotFound, + // expectedContent: "404 Not Found", + // URLPath: "/catalogs/non-existent-catalog/api/v1/all", + // }, + { + name: "Server returns 200 when path '/catalogs//api/v1/all' is queried, when catalog exists", + expectedStatusCode: http.StatusOK, + expectedContent: `{"foo":"bar"}`, + URLPath: "/catalogs/test-catalog/api/v1/all", + }, + } { + t.Run(tc.name, func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/%s", testServer.URL, tc.URLPath), nil) + require.NoError(t, err) + req.Header.Set("Accept-Encoding", "gzip") + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + require.Equal(t, tc.expectedStatusCode, resp.StatusCode) + + var actualContent []byte + actualContent, err = io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, tc.expectedContent, strings.TrimSpace(string(actualContent))) + require.NoError(t, resp.Body.Close()) + }) + } +} + +func TestSqliteServerQueryEndpoint(t *testing.T) { + store, err := NewSQLiteV1(t.TempDir(), &url.URL{Path: urlPrefix}) + require.NoError(t, err) + if store.Store(context.Background(), "test-catalog", createTestFS(t)) != nil { + t.Fatal("failed to store test catalog") + } + testServer := httptest.NewServer(store.StorageServerHandler()) + + testCases := []struct { + name string + setupStore func() (*httptest.Server, error) + queryParams string + expectedStatusCode int + expectedContent string + }{ + { + name: "valid query with package schema", + queryParams: "?schema=olm.package", + expectedStatusCode: http.StatusOK, + expectedContent: `{"defaultChannel":"preview_test","name":"webhook_operator_test","schema":"olm.package"}`, + }, + { + name: "valid query with schema and name combination", + queryParams: "?schema=olm.package&name=webhook_operator_test", + expectedStatusCode: http.StatusOK, + expectedContent: `{"defaultChannel":"preview_test","name":"webhook_operator_test","schema":"olm.package"}`, + }, + { + name: "valid query with channel schema and package name combination", + queryParams: "?schema=olm.channel&package=webhook_operator_test", + expectedStatusCode: http.StatusOK, + expectedContent: `{"entries":[{"name":"bundle.v0.0.1"}],"name":"preview_test","package":"webhook_operator_test","schema":"olm.channel"}`, + }, + { + name: "query with all meta fields", + queryParams: "?schema=olm.bundle&package=webhook_operator_test&name=bundle.v0.0.1", + expectedStatusCode: http.StatusOK, + expectedContent: `{"image":"quaydock.io/namespace/bundle:0.0.3","name":"bundle.v0.0.1","package":"webhook_operator_test","properties":[{"type":"olm.bundle.object","value":{"data":"dW5pbXBvcnRhbnQK"}},{"type":"some.other","value":{"data":"arbitrary-info"}}],"relatedImages":[{"image":"testimage:latest","name":"test"}],"schema":"olm.bundle"}`, + }, + // { + // name: "valid query for package schema for a package that does not exist", + // queryParams: "?schema=olm.package&name=not-present", + // expectedStatusCode: http.StatusOK, + // expectedContent: "", + // }, + { + name: "valid query with package and name", + queryParams: "?package=webhook_operator_test&name=bundle.v0.0.1", + expectedStatusCode: http.StatusOK, + expectedContent: `{"image":"quaydock.io/namespace/bundle:0.0.3","name":"bundle.v0.0.1","package":"webhook_operator_test","properties":[{"type":"olm.bundle.object","value":{"data":"dW5pbXBvcnRhbnQK"}},{"type":"some.other","value":{"data":"arbitrary-info"}}],"relatedImages":[{"image":"testimage:latest","name":"test"}],"schema":"olm.bundle"}`, + }, + // { + // name: "invalid query with non-existent schema", + // queryParams: "?schema=non_existent_schema", + // expectedStatusCode: http.StatusNotFound, + // expectedContent: "400 Bad Request", + // }, + // { + // name: "cached response with If-Modified-Since", + // queryParams: "?schema=olm.package", + // expectedStatusCode: http.StatusNotModified, + // expectedContent: "", + // }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/catalogs/test-catalog/api/v1/query%s", testServer.URL, tc.queryParams), nil) + require.NoError(t, err) + + if strings.Contains(tc.name, "If-Modified-Since") { + // Do an initial request to get a Last-Modified timestamp + // for the actual request + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + req.Header.Set("If-Modified-Since", resp.Header.Get("Last-Modified")) + } + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, tc.expectedStatusCode, resp.StatusCode) + + actualContent, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, tc.expectedContent, strings.TrimSpace(string(actualContent))) + }) + } +} + +func TestSqliteServerLoadHandling(t *testing.T) { + store, err := NewSQLiteV1(t.TempDir(), &url.URL{Path: urlPrefix}) + require.NoError(t, err) + + // Create large test data + largeFS := fstest.MapFS{} + for i := 0; i < 1000; i++ { + largeFS[fmt.Sprintf("meta_%d.json", i)] = &fstest.MapFile{ + Data: []byte(fmt.Sprintf(`{"schema":"olm.bundle","package":"test-op-%d","name":"test-op.v%d.0"}`, i, i)), + } + } + + if err := store.Store(context.Background(), "test-catalog", largeFS); err != nil { + t.Fatal("failed to store test catalog") + } + + testServer := httptest.NewServer(store.StorageServerHandler()) + defer testServer.Close() + + tests := []struct { + name string + concurrent int + requests func(baseURL string) []*http.Request + validateFunc func(t *testing.T, responses []*http.Response, errs []error) + }{ + { + name: "concurrent identical queries", + concurrent: 100, + requests: func(baseURL string) []*http.Request { + var reqs []*http.Request + for i := 0; i < 100; i++ { + req, _ := http.NewRequest(http.MethodGet, + fmt.Sprintf("%s/catalogs/test-catalog/api/v1/query?schema=olm.bundle", baseURL), + nil) + req.Header.Set("Accept", "application/jsonl") + reqs = append(reqs, req) + } + return reqs + }, + validateFunc: func(t *testing.T, responses []*http.Response, errs []error) { + for _, err := range errs { + require.NoError(t, err) + } + for _, resp := range responses { + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, resp.Header.Get("Content-Type"), "application/jsonl") + resp.Body.Close() + } + }, + }, + { + name: "concurrent different queries", + concurrent: 50, + requests: func(baseURL string) []*http.Request { + var reqs []*http.Request + for i := 0; i < 50; i++ { + req, _ := http.NewRequest(http.MethodGet, + fmt.Sprintf("%s/catalogs/test-catalog/api/v1/query?package=test-op-%d", baseURL, i), + nil) + req.Header.Set("Accept", "application/jsonl") + reqs = append(reqs, req) + } + return reqs + }, + validateFunc: func(t *testing.T, responses []*http.Response, errs []error) { + for _, err := range errs { + require.NoError(t, err) + } + for _, resp := range responses { + require.Equal(t, http.StatusOK, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, string(body), "test-op-") + resp.Body.Close() + } + }, + }, + { + name: "mixed all and query endpoints", + concurrent: 40, + requests: func(baseURL string) []*http.Request { + var reqs []*http.Request + for i := 0; i < 20; i++ { + allReq, _ := http.NewRequest(http.MethodGet, + fmt.Sprintf("%s/catalogs/test-catalog/api/v1/all", baseURL), + nil) + queryReq, _ := http.NewRequest(http.MethodGet, + fmt.Sprintf("%s/catalogs/test-catalog/api/v1/query?schema=olm.bundle", baseURL), + nil) + allReq.Header.Set("Accept", "application/jsonl") + queryReq.Header.Set("Accept", "application/jsonl") + reqs = append(reqs, allReq, queryReq) + } + return reqs + }, + validateFunc: func(t *testing.T, responses []*http.Response, errs []error) { + for _, err := range errs { + require.NoError(t, err) + } + for _, resp := range responses { + require.Equal(t, http.StatusOK, resp.StatusCode) + resp.Body.Close() + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + wg sync.WaitGroup + responses = make([]*http.Response, tt.concurrent) + errs = make([]error, tt.concurrent) + ) + + requests := tt.requests(testServer.URL) + for i := 0; i < tt.concurrent; i++ { + wg.Add(1) + go func(idx int) { + defer wg.Done() + resp, err := http.DefaultClient.Do(requests[idx]) + responses[idx] = resp + errs[idx] = err + }(i) + } + + wg.Wait() + tt.validateFunc(t, responses, errs) + }) + } +} + +func createTestFS(t *testing.T) fs.FS { + t.Helper() + testBundleTemplate := `--- +image: %s +name: %s +schema: olm.bundle +package: %s +relatedImages: + - name: %s + image: %s +properties: + - type: olm.bundle.object + value: + data: %s + - type: some.other + value: + data: arbitrary-info +` + + testPackageTemplate := `--- +defaultChannel: %s +name: %s +schema: olm.package +` + + testChannelTemplate := `--- +schema: olm.channel +package: %s +name: %s +entries: + - name: %s +` + testBundleName := "bundle.v0.0.1" + testBundleImage := "quaydock.io/namespace/bundle:0.0.3" + testBundleRelatedImageName := "test" + testBundleRelatedImageImage := "testimage:latest" + testBundleObjectData := "dW5pbXBvcnRhbnQK" + testPackageDefaultChannel := "preview_test" + testPackageName := "webhook_operator_test" + testChannelName := "preview_test" + + testPackage := fmt.Sprintf(testPackageTemplate, testPackageDefaultChannel, testPackageName) + testBundle := fmt.Sprintf(testBundleTemplate, testBundleImage, testBundleName, testPackageName, testBundleRelatedImageName, testBundleRelatedImageImage, testBundleObjectData) + testChannel := fmt.Sprintf(testChannelTemplate, testPackageName, testChannelName, testBundleName) + return &fstest.MapFS{ + "bundle.yaml": {Data: []byte(testBundle), Mode: os.ModePerm}, + "package.yaml": {Data: []byte(testPackage), Mode: os.ModePerm}, + "channel.yaml": {Data: []byte(testChannel), Mode: os.ModePerm}, + } +} diff --git a/go.mod b/go.mod index 0fab3290f..4b7118965 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,7 @@ require ( k8s.io/component-base v0.32.0 k8s.io/klog/v2 v2.130.1 k8s.io/utils v0.0.0-20241210054802-24370beab758 + modernc.org/sqlite v1.34.5 sigs.k8s.io/controller-runtime v0.19.4 sigs.k8s.io/yaml v1.4.0 ) @@ -88,6 +89,7 @@ require ( github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.11.2 // indirect github.com/evanphx/json-patch v5.9.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect @@ -173,6 +175,7 @@ require ( github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/runtime-spec v1.2.0 // indirect @@ -186,6 +189,7 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.57.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rubenv/sql-migrate v1.7.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -245,6 +249,9 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect k8s.io/kubectl v0.32.0 // indirect + modernc.org/libc v1.55.3 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect oras.land/oras-go v1.2.5 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect diff --git a/go.sum b/go.sum index 1f5d51bc8..c2886ace2 100644 --- a/go.sum +++ b/go.sum @@ -177,6 +177,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.11.2 h1:1onLa9DcsMYO9P+CXaL0dStDqQ2EHHXLiz+BtnqkLAU= github.com/emicklei/go-restful/v3 v3.11.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -509,6 +511,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= @@ -594,6 +598,8 @@ github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -1014,6 +1020,30 @@ k8s.io/kubectl v0.32.0 h1:rpxl+ng9qeG79YA4Em9tLSfX0G8W0vfaiPVrc/WR7Xw= k8s.io/kubectl v0.32.0/go.mod h1:qIjSX+QgPQUgdy8ps6eKsYNF+YmFOAO3WygfucIqFiE= k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= +modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= +modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g= +modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo= oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=