Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bigquery): expose customer managed encryption key for ML models #9302

26 changes: 26 additions & 0 deletions bigquery/google/cloud/bigquery/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from google.api_core import datetime_helpers
from google.cloud.bigquery import _helpers
from google.cloud.bigquery_v2 import types
from google.cloud.bigquery.table import EncryptionConfiguration
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems wrong for EncryptionConfiguration to live in the table module now that it's used for tables and models. Let's create a new encryption_configuration model for this to live. (Similar to external_config.)

We can make an alias in the table module to keep this from being a breaking change.



class Model(object):
Expand All @@ -51,6 +52,7 @@ class Model(object):
# have an exhaustive list of all mutable properties.
"labels": "labels",
"description": "description",
"encryption_configuration": "encryptionConfiguration",
}

def __init__(self, model_ref):
Expand Down Expand Up @@ -256,6 +258,30 @@ def labels(self, value):
value = {}
self._properties["labels"] = value

@property
def encryption_configuration(self):
"""google.cloud.bigquery.table.EncryptionConfiguration: Custom
encryption configuration for the model.

Custom encryption configuration (e.g., Cloud KMS keys) or :data:`None`
if using default encryption.

See `protecting data with Cloud KMS keys
<https://cloud.google.com/bigquery/docs/customer-managed-encryption>`_
in the BigQuery documentation.
"""
prop = self._properties.get("encryptionConfiguration")
if prop:
prop = EncryptionConfiguration.from_api_repr(prop)
return prop

@encryption_configuration.setter
def encryption_configuration(self, value):
api_repr = value
if value:
api_repr = value.to_api_repr()
self._properties["encryptionConfiguration"] = api_repr

@classmethod
def from_api_repr(cls, resource):
"""Factory: construct a model resource given its API representation
Expand Down
28 changes: 28 additions & 0 deletions bigquery/tests/unit/model/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import google.cloud._helpers
from google.cloud.bigquery_v2.gapic import enums

KMS_KEY_NAME = "projects/1/locations/global/keyRings/1/cryptoKeys/1"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is a unit test, but we're supposed to discourage use of global location. Let's change this to us.



@pytest.fixture
def target_class():
Expand Down Expand Up @@ -99,6 +101,7 @@ def test_from_api_repr(target_class):
},
],
"featureColumns": [],
"encryptionConfiguration": {"kmsKeyName": KMS_KEY_NAME},
}
got = target_class.from_api_repr(resource)

Expand All @@ -116,6 +119,7 @@ def test_from_api_repr(target_class):
assert got.friendly_name == u"A friendly name."
assert got.model_type == enums.Model.ModelType.LOGISTIC_REGRESSION
assert got.labels == {"greeting": u"こんにちは"}
assert got.encryption_configuration.kms_key_name == KMS_KEY_NAME
assert got.training_runs[0].training_options.initial_learn_rate == 1.0
assert (
got.training_runs[0]
Expand Down Expand Up @@ -160,6 +164,7 @@ def test_from_api_repr_w_minimal_resource(target_class):
assert got.friendly_name is None
assert got.model_type == enums.Model.ModelType.MODEL_TYPE_UNSPECIFIED
assert got.labels == {}
assert got.encryption_configuration is None
assert len(got.training_runs) == 0
assert len(got.feature_columns) == 0
assert len(got.label_columns) == 0
Expand Down Expand Up @@ -229,6 +234,17 @@ def test_from_api_repr_w_unknown_fields(target_class):
["labels"],
{"labels": {"a-label": "a-value"}},
),
(
{
"friendlyName": "hello",
"description": "world",
"expirationTime": None,
"labels": {"a-label": "a-value"},
"encryptionConfiguration": {"kmsKeyName": KMS_KEY_NAME},
},
["encryptionConfiguration"],
{"encryptionConfiguration": {"kmsKeyName": KMS_KEY_NAME}},
),
],
)
def test_build_resource(object_under_test, resource, filter_fields, expected):
Expand Down Expand Up @@ -283,6 +299,18 @@ def test_replace_labels(object_under_test):
assert object_under_test.labels == {}


def test_set_encryption_configuration(object_under_test):
from google.cloud.bigquery.table import EncryptionConfiguration

assert not object_under_test.encryption_configuration
object_under_test.encryption_configuration = EncryptionConfiguration(
kms_key_name=KMS_KEY_NAME
)
assert object_under_test.encryption_configuration.kms_key_name == KMS_KEY_NAME
object_under_test.encryption_configuration = None
assert not object_under_test.encryption_configuration


def test_repr(target_class):
model = target_class("my-proj.my_dset.my_model")
got = repr(model)
Expand Down