// Copyright 2023 The Sigstore Authors. // // 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 mysql import ( "context" "errors" "fmt" "github.com/jmoiron/sqlx" "github.com/sigstore/rekor/pkg/log" // this imports the mysql driver for go _ "github.com/go-sql-driver/mysql" ) const ( ProviderType = "mysql" lookupStmt = "SELECT EntryUUID FROM EntryIndex WHERE EntryKey IN (?)" writeStmt = "INSERT IGNORE INTO EntryIndex (EntryKey, EntryUUID) VALUES (:key, :uuid)" createTableStmt = `CREATE TABLE IF NOT EXISTS EntryIndex ( PK BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, EntryKey varchar(512) NOT NULL, EntryUUID char(80) NOT NULL, PRIMARY KEY(PK), UNIQUE(EntryKey, EntryUUID) )` ) // IndexStorageProvider implements indexstorage.IndexStorage type IndexStorageProvider struct { db *sqlx.DB } func NewProvider(dsn string, opts ...Options) (*IndexStorageProvider, error) { var err error provider := &IndexStorageProvider{} provider.db, err = sqlx.Open(ProviderType, dsn) if err != nil { return nil, err } if err = provider.db.Ping(); err != nil { return nil, err } for _, o := range opts { o.applyConnMaxIdleTime(provider.db) o.applyConnMaxLifetime(provider.db) o.applyMaxIdleConns(provider.db) o.applyMaxOpenConns(provider.db) } if _, err := provider.db.Exec(createTableStmt); err != nil { return nil, fmt.Errorf("create table if not exists failed: %w", err) } return provider, nil } // LookupIndices looks up and returns all indices for the specified keys. The key value(s) will be canonicalized // by converting all characters into a lowercase value before looking up in Redis func (isp *IndexStorageProvider) LookupIndices(ctx context.Context, keys []string) ([]string, error) { if isp.db == nil { return []string{}, errors.New("sql client has not been initialized") } query, args, err := sqlx.In(lookupStmt, keys) if err != nil { return []string{}, fmt.Errorf("error preparing statement: %w", err) } rows, err := isp.db.QueryContext(ctx, isp.db.Rebind(query), args...) if err != nil { return []string{}, fmt.Errorf("error looking up indices from mysql: %w", err) } defer rows.Close() var entryUUIDs []string for rows.Next() { var result string if err := rows.Scan(&result); err != nil { return []string{}, fmt.Errorf("error parsing results from mysql: %w", err) } entryUUIDs = append(entryUUIDs, result) } if err := rows.Err(); err != nil { return []string{}, fmt.Errorf("error processing results from mysql: %w", err) } return entryUUIDs, nil } // WriteIndex adds the index for the specified key. The key value will be canonicalized // by converting all characters into a lowercase value before appending the index in Redis func (isp *IndexStorageProvider) WriteIndex(ctx context.Context, keys []string, index string) error { if isp.db == nil { return errors.New("sql client has not been initialized") } var valueMaps []map[string]interface{} for _, key := range keys { valueMaps = append(valueMaps, map[string]interface{}{"key": key, "uuid": index}) } result, err := isp.db.NamedExecContext(ctx, writeStmt, valueMaps) if err != nil { return fmt.Errorf("mysql write error: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("mysql error getting rowsAffected: %w", err) } log.ContextLogger(ctx).Debugf("WriteIndex affected %d rows", rowsAffected) return nil } // Shutdown cleans up any client resources that may be held by the provider func (isp *IndexStorageProvider) Shutdown() error { if isp.db == nil { return nil } return isp.db.Close() }