-
Notifications
You must be signed in to change notification settings - Fork 240
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add IssuanceChainStorage MySQL implementation (#1462)
* Add IssuanceChainStorage MySQL implementation * Add err check for issuanceChainStorage.Add in test * Add strict SQL mode comment * Panic when the MySQL database cannot be opened * Add a separate error message for mysql data source name prefix check * Replace `panic` with `klog.Exitf`
- Loading branch information
Showing
5 changed files
with
244 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
// Copyright 2024 Google LLC | ||
// | ||
// 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 defines the IssuanceChainStorage type, which implements IssuanceChainStorage interface with FindByKey and Add methods. | ||
package mysql | ||
|
||
import ( | ||
"context" | ||
"database/sql" | ||
"errors" | ||
"fmt" | ||
"strings" | ||
|
||
"k8s.io/klog/v2" | ||
|
||
"github.com/go-sql-driver/mysql" | ||
) | ||
|
||
const ( | ||
selectIssuanceChainByKeySQL = "SELECT c.ChainValue FROM IssuanceChain AS c WHERE c.IdentityHash = ?" | ||
insertIssuanceChainSQL = "INSERT INTO IssuanceChain(IdentityHash, ChainValue) VALUES (?, ?)" | ||
) | ||
|
||
type IssuanceChainStorage struct { | ||
db *sql.DB | ||
} | ||
|
||
// NewIssuanceChainStorage takes the database connection string as the input and return the IssuanceChainStorage. | ||
func NewIssuanceChainStorage(ctx context.Context, dbConn string) *IssuanceChainStorage { | ||
db, err := open(ctx, dbConn) | ||
if err != nil { | ||
klog.Exitf(fmt.Sprintf("failed to open database: %v", err)) | ||
} | ||
|
||
return &IssuanceChainStorage{ | ||
db: db, | ||
} | ||
} | ||
|
||
// FindByKey returns the key-value pair of issuance chain by the key. | ||
func (s *IssuanceChainStorage) FindByKey(ctx context.Context, key []byte) ([]byte, error) { | ||
row := s.db.QueryRowContext(ctx, selectIssuanceChainByKeySQL, key) | ||
if err := row.Err(); err != nil { | ||
return nil, err | ||
} | ||
|
||
var chain []byte | ||
if err := row.Scan(&chain); err != nil { | ||
return nil, err | ||
} | ||
|
||
return chain, nil | ||
} | ||
|
||
// Add inserts the key-value pair of issuance chain. | ||
func (s *IssuanceChainStorage) Add(ctx context.Context, key []byte, chain []byte) error { | ||
_, err := s.db.ExecContext(ctx, insertIssuanceChainSQL, key, chain) | ||
if err != nil { | ||
// Ignore duplicated key error. | ||
var mysqlErr *mysql.MySQLError | ||
if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 { | ||
return nil | ||
} | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// open takes the data source name and returns the sql.DB object. | ||
func open(ctx context.Context, dataSourceName string) (*sql.DB, error) { | ||
// Verify data source name format. | ||
conn := strings.Split(dataSourceName, "://") | ||
if len(conn) != 2 { | ||
return nil, errors.New("could not parse MySQL data source name") | ||
} | ||
if conn[0] != "mysql" { | ||
return nil, errors.New("expect data source name to start with mysql") | ||
} | ||
|
||
db, err := sql.Open("mysql", conn[1]) | ||
if err != nil { | ||
// Don't log data source name as it could contain credentials. | ||
klog.Errorf("could not open MySQL database, check config: %s", err) | ||
return nil, err | ||
} | ||
|
||
// Enable strict SQL mode to ensure consistent behaviour among different storage engines when handling invalid or missing values in data-change statements. | ||
if _, err := db.ExecContext(ctx, "SET sql_mode = 'STRICT_ALL_TABLES'"); err != nil { | ||
klog.Warningf("failed to set strict mode on mysql db: %s", err) | ||
return nil, err | ||
} | ||
|
||
return db, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
// Copyright 2024 Google LLC | ||
// | ||
// 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 ( | ||
"bytes" | ||
"context" | ||
"crypto/sha256" | ||
"database/sql" | ||
"os" | ||
"testing" | ||
|
||
"github.com/DATA-DOG/go-sqlmock" | ||
) | ||
|
||
func TestIssuanceChainFindByKeySuccess(t *testing.T) { | ||
db, mock, err := sqlmock.New() | ||
if err != nil { | ||
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) | ||
} | ||
defer db.Close() | ||
|
||
testVal := readTestData(t, "leaf00.chain") | ||
testKey := sha256.Sum256(testVal) | ||
|
||
issuanceChainMockRows := sqlmock.NewRows([]string{"ChainValue"}).AddRow(testVal) | ||
mock.ExpectQuery(selectIssuanceChainByKeySQL).WillReturnRows(issuanceChainMockRows) | ||
|
||
storage := mockIssuanceChainStorage(db) | ||
got, err := storage.FindByKey(context.Background(), testKey[:]) | ||
if err != nil { | ||
t.Errorf("issuanceChainStorage.FindByKey: %v", err) | ||
} | ||
if !bytes.Equal(got, testVal) { | ||
t.Errorf("got: %v, want: %v", got, testVal) | ||
} | ||
|
||
if err := mock.ExpectationsWereMet(); err != nil { | ||
t.Errorf("there were unfulfilled expectations: %s", err) | ||
} | ||
} | ||
|
||
func TestIssuanceChainAddSuccess(t *testing.T) { | ||
db, mock, err := sqlmock.New() | ||
if err != nil { | ||
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) | ||
} | ||
defer db.Close() | ||
|
||
tests := setupTestData(t, | ||
"leaf00.chain", | ||
"leaf01.chain", | ||
"leaf02.chain", | ||
) | ||
|
||
storage := mockIssuanceChainStorage(db) | ||
for k, v := range tests { | ||
mock.ExpectExec("INSERT INTO IssuanceChain").WithArgs([]byte(k), v).WillReturnResult(sqlmock.NewResult(1, 1)) | ||
if err := storage.Add(context.Background(), []byte(k), v); err != nil { | ||
t.Errorf("issuanceChainStorage.Add: %v", err) | ||
} | ||
} | ||
|
||
if err := mock.ExpectationsWereMet(); err != nil { | ||
t.Errorf("there were unfulfilled expectations: %s", err) | ||
} | ||
} | ||
|
||
func readTestData(t *testing.T, filename string) []byte { | ||
t.Helper() | ||
|
||
data, err := os.ReadFile("../../../testdata/" + filename) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
return data | ||
} | ||
|
||
func setupTestData(t *testing.T, filenames ...string) map[string][]byte { | ||
t.Helper() | ||
|
||
data := make(map[string][]byte, len(filenames)) | ||
|
||
for _, filename := range filenames { | ||
val := readTestData(t, filename) | ||
key := sha256.Sum256(val) | ||
data[string(key[:])] = val | ||
} | ||
|
||
return data | ||
} | ||
|
||
func mockIssuanceChainStorage(db *sql.DB) *IssuanceChainStorage { | ||
return &IssuanceChainStorage{ | ||
db: db, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
-- Copyright 2024 Google LLC | ||
-- | ||
-- 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. | ||
|
||
-- MySQL / MariaDB version of the CTFE database schema | ||
|
||
-- "IssuanceChain" table contains the hash and value pairs of the issuance chain. | ||
CREATE TABLE IF NOT EXISTS `IssuanceChain` ( | ||
-- Hash of the chain of intermediate certificates and root certificates. | ||
`IdentityHash` VARBINARY(255) NOT NULL, | ||
-- Chain data of intermediate certificates and root certificates. | ||
`ChainValue` LONGBLOB NOT NULL, | ||
PRIMARY KEY (`IdentityHash`) | ||
); |