Skip to content

Commit

Permalink
Merge branch 'main' into opensearch-ranger-relation
Browse files Browse the repository at this point in the history
  • Loading branch information
AmberCharitos authored Jul 24, 2024
2 parents 2255257 + 1ba4200 commit 0f15f1d
Show file tree
Hide file tree
Showing 13 changed files with 195 additions and 19 deletions.
19 changes: 18 additions & 1 deletion config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
options:
ranger-admin-password:
description: |
Password used for the policy manager (Web interface)
The password for Ranger Admin user.
Password can not be changed using this property after initial deployment.
It can be changed in the UI.
Password should be minimum 8 characters with min one alphabet and one numeric.
default: "rangerR0cks!"
type: string
tls-secret-name:
Expand Down Expand Up @@ -116,3 +119,17 @@ options:
The function the charm should provide, either `admin` or `usersync`.
type: string
default: admin
lookup-timeout:
description: |
The default timeout for the resource auto-complete
functionality for Ranger service in ms.
type: int
default: 3000
ranger-usersync-password:
description: |
The password for the user that synchronizes users and groups from LDAP to Ranger admin.
Password can not be changed using this property after initial deployment.
It can be changed in the UI.
Password should be minimum 8 characters with min one alphabet and one numeric.
type: string
default: rangerR0cks!
29 changes: 28 additions & 1 deletion lib/charms/data_platform_libs/v0/data_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ def _on_topic_requested(self, event: TopicRequestedEvent):

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 36
LIBPATCH = 38

PYDEPS = ["ops>=2.0.0"]

Expand Down Expand Up @@ -658,6 +658,10 @@ def set_content(self, content: Dict[str, str]) -> None:
if not self.meta:
return

# DPE-4182: do not create new revision if the content stay the same
if content == self.get_content():
return

if content:
self._move_to_new_label_if_needed()
self.meta.set_content(content)
Expand Down Expand Up @@ -2602,6 +2606,14 @@ def set_version(self, relation_id: int, version: str) -> None:
"""
self.update_relation_data(relation_id, {"version": version})

def set_subordinated(self, relation_id: int) -> None:
"""Raises the subordinated flag in the application relation databag.
Args:
relation_id: the identifier for a particular relation.
"""
self.update_relation_data(relation_id, {"subordinated": "true"})


class DatabaseProviderEventHandlers(EventHandlers):
"""Provider-side of the database relation handlers."""
Expand Down Expand Up @@ -2838,6 +2850,21 @@ def _on_relation_created_event(self, event: RelationCreatedEvent) -> None:

def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
"""Event emitted when the database relation has changed."""
is_subordinate = False
remote_unit_data = None
for key in event.relation.data.keys():
if isinstance(key, Unit) and not key.name.startswith(self.charm.app.name):
remote_unit_data = event.relation.data[key]
elif isinstance(key, Application) and key.name != self.charm.app.name:
is_subordinate = event.relation.data[key].get("subordinated") == "true"

if is_subordinate:
if not remote_unit_data:
return

if remote_unit_data.get("state") != "ready":
return

# Check which data has changed to emit customs events.
diff = self._diff(event)

Expand Down
3 changes: 2 additions & 1 deletion lib/charms/grafana_k8s/v0/grafana_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ def __init__(self, *args):
# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version

LIBPATCH = 35
LIBPATCH = 36

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -1050,6 +1050,7 @@ def __init__(

self.framework.observe(self._charm.on.leader_elected, self._update_all_dashboards_from_dir)
self.framework.observe(self._charm.on.upgrade_charm, self._update_all_dashboards_from_dir)
self.framework.observe(self._charm.on.config_changed, self._update_all_dashboards_from_dir)

self.framework.observe(
self._charm.on[self._relation_name].relation_created,
Expand Down
23 changes: 16 additions & 7 deletions lib/charms/loki_k8s/v0/loki_push_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
applications such as pebble, or charmed operators of workloads such as grafana-agent or promtail,
that can communicate with loki directly.
- `LogProxyConsumer`: This object can be used by any Charmed Operator which needs to
send telemetry, such as logs, to Loki through a Log Proxy by implementing the consumer side of the
`loki_push_api` relation interface.
- `LogProxyConsumer`: DEPRECATED.
This object can be used by any Charmed Operator which needs to send telemetry, such as logs, to
Loki through a Log Proxy by implementing the consumer side of the `loki_push_api` relation
interface.
Filtering logs in Loki is largely performed on the basis of labels. In the Juju ecosystem, Juju
topology labels are used to uniquely identify the workload which generates telemetry like logs.
Expand All @@ -38,13 +39,14 @@
- `charm`: A reference to the parent (Loki) charm.
- `relation_name`: The name of the relation that the charm uses to interact
with its clients, which implement `LokiPushApiConsumer` or `LogProxyConsumer`.
with its clients, which implement `LokiPushApiConsumer` or `LogProxyConsumer`
(note that LogProxyConsumer is deprecated).
If provided, this relation name must match a provided relation in metadata.yaml with the
`loki_push_api` interface.
The default relation name is "logging" for `LokiPushApiConsumer` and "log-proxy" for
`LogProxyConsumer`.
`LogProxyConsumer` (note that LogProxyConsumer is deprecated).
For example, a provider's `metadata.yaml` file may look as follows:
Expand Down Expand Up @@ -219,6 +221,9 @@ def __init__(self, *args):
## LogProxyConsumer Library Usage
> Note: This object is deprecated. Consider migrating to LogForwarder (see v1/loki_push_api) with
> the release of Juju 3.6 LTS.
Let's say that we have a workload charm that produces logs, and we need to send those logs to a
workload implementing the `loki_push_api` interface, such as `Loki` or `Grafana Agent`.
Expand Down Expand Up @@ -480,7 +485,7 @@ def _alert_rules_error(self, event):

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 29
LIBPATCH = 30

PYDEPS = ["cosl"]

Expand Down Expand Up @@ -1539,7 +1544,8 @@ def __init__(
the Loki API endpoint to push logs. It is intended for workloads that can speak
loki_push_api (https://grafana.com/docs/loki/latest/api/#push-log-entries-to-loki), such
as grafana-agent.
(If you only need to forward a few workload log files, then use LogProxyConsumer.)
(If you need to forward workload stdout logs, then use v1/loki_push_api.LogForwarder; if
you need to forward log files, then use LogProxyConsumer.)
`LokiPushApiConsumer` can be instantiated as follows:
Expand Down Expand Up @@ -1728,6 +1734,9 @@ class LogProxyEvents(ObjectEvents):
class LogProxyConsumer(ConsumerBase):
"""LogProxyConsumer class.
> Note: This object is deprecated. Consider migrating to v1/loki_push_api.LogForwarder with the
> release of Juju 3.6 LTS.
The `LogProxyConsumer` object provides a method for attaching `promtail` to
a workload in order to generate structured logging data from applications
which traditionally log to syslog or do not have native Loki integration.
Expand Down
40 changes: 40 additions & 0 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ def _configure_ranger_admin(self, container):
"OPENSEARCH_ENABLED": opensearch.get("is_enabled"),
"RANGER_ADMIN_PWD": self.config["ranger-admin-password"],
"JAVA_OPTS": f"-Duser.timezone=UTC0 -Djavax.net.ssl.trustStorePassword={self._state.truststore_pwd}",
"RANGER_USERSYNC_PWD": self.config["ranger-usersync-password"],
}
logger.info(context)
config = render("admin-config.jinja", context)
Expand Down Expand Up @@ -309,6 +310,7 @@ def _configure_ranger_usersync(self, container):
context.update(
{
"POLICY_MGR_URL": self.config["policy-mgr-url"],
"RANGER_USERSYNC_PWD": self.config["ranger-usersync-password"],
}
)
config = render("ranger-usersync-config.jinja", context)
Expand All @@ -319,6 +321,29 @@ def _configure_ranger_usersync(self, container):
)
return USERSYNC_ENTRYPOINT, context

def _validate_password(self, password, config_key, state_key):
"""Validate that the admin and usersync passwords are not changed after deployment.
Args:
password: the deployment password.
config_key: the config key for the password.
state_key: the key the password is stored in state.
Raises:
ValueError: in case the password has been changed.
"""
if password is None:
if self.unit.is_leader():
setattr(self._state, state_key, self.config[config_key])
logger.info(self._state.ranger_admin_password)
elif password != self.config[config_key]:
message = (
f"value of '{config_key}' config cannot be changed after deployment. "
f"Value should be {password}"
)
logger.error(message)
raise ValueError(message)

def validate(self):
"""Validate that configuration and relations are valid and ready.
Expand All @@ -334,6 +359,21 @@ def validate(self):
if self.config["charm-function"].value == "usersync":
self.ldap.validate()

ranger_admin_password = self._state.ranger_admin_password
ranger_usersync_password = self._state.ranger_usersync_password

logger.info(ranger_admin_password)
self._validate_password(
ranger_admin_password,
"ranger-admin-password",
"ranger_admin_password",
)
self._validate_password(
ranger_usersync_password,
"ranger-usersync-password",
"ranger_usersync_password",
)

def update(self, event):
"""Update the Ranger server configuration and re-plan its execution.
Expand Down
3 changes: 3 additions & 0 deletions src/relations/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ def _create_ranger_service(self, ranger, data, event):
)
service.configs = {
"username": f"relation_id_{event.relation.id}",
"resource.lookup.timeout.value.in.ms": self.charm.config[
"lookup-timeout"
],
}
for key, value in data.items():
if key not in ["name", "type"]:
Expand Down
47 changes: 46 additions & 1 deletion src/structured_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,10 @@ class CharmConfig(BaseConfigModel):
sync_ldap_user_group_name_attribute: Optional[str]
sync_ldap_deltasync: bool
sync_interval: Optional[int]
ranger_usersync_password: Optional[str]
ranger_usersync_password: str
policy_mgr_url: str
charm_function: FunctionType
lookup_timeout: int

@validator("*", pre=True)
@classmethod
Expand Down Expand Up @@ -113,3 +114,47 @@ def sync_ldap_url_validator(cls, value: str) -> Optional[str]:
if re.match(ldap_url_pattern, value) is not None:
return value
raise ValueError("Value incorrectly formatted.")

@validator("lookup_timeout")
@classmethod
def lookup_timeout_validator(cls, value: str) -> Optional[int]:
"""Check validity of `lookup_timeout` field.
Args:
value: timeout value
Returns:
int_value: integer for service configuration
Raises:
ValueError: in the case when the value is out of range
"""
int_value = int(value)
if 1000 <= int_value <= 10000:
return int_value
raise ValueError("Value out of range.")

@validator("ranger_admin_password", "ranger_usersync_password")
@classmethod
def password_validator(cls, value: str) -> str:
"""Validate if the password meets the following requirements.
- Minimum 8 characters in length
- Contains at least one alphabetic character
- Contains at least one numeric character
Args:
value: The password to validate.
Returns:
value: The validated password if it meets the requirements.
Raises:
ValueError: If the password does not meet the requirements.
"""
pattern = re.compile(
r"^(?=.*[A-Za-z])(?=.*\d)(?=.*[\W_])[A-Za-z\d\W_]{8,}$"
)
if pattern.match(value):
return value
raise ValueError("Password does not match requirements.")
8 changes: 4 additions & 4 deletions templates/admin-config.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ db_password={{ DB_PWD }}

# change password. Password for below mentioned users can be changed only once using this property.
#PLEASE NOTE :: Password should be minimum 8 characters with min one alphabet and one numeric.
rangerAdmin_password={{ RANGER_ADMIN_PWD | default("rangerR0cks!") }}
rangerTagsync_password={{ RANGER_ADMIN_PWD | default("rangerR0cks!") }}
rangerUsersync_password={{ RANGER_ADMIN_PWD | default("rangerR0cks!") }}
keyadmin_password={{ RANGER_ADMIN_PWD | default("rangerR0cks!") }}
rangerAdmin_password={{ RANGER_ADMIN_PWD }}
rangerTagsync_password={{ RANGER_ADMIN_PWD }}
rangerUsersync_password={{ RANGER_USERSYNC_PWD }}
keyadmin_password={{ RANGER_ADMIN_PWD }}


#Source for Audit Store. Currently solr and elasticsearch are supported.
Expand Down
2 changes: 1 addition & 1 deletion templates/ranger-usersync-config.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ unix_group=ranger

# This password should be changed from the default.
# Please note that this password should be as per rangerusersync user in ranger admin.
rangerUsersync_password= rangerR0cks!
rangerUsersync_password= {{ RANGER_USERSYNC_PWD }}

hadoop_conf=/etc/hadoop/conf

Expand Down
1 change: 1 addition & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ async def deploy(ops_test: OpsTest):
resources=resources,
application_name=APP_NAME,
num_units=1,
config={"ranger-usersync-password": "P@ssw0rd1234"},
)

await ops_test.model.wait_for_idle(
Expand Down
1 change: 1 addition & 0 deletions tests/integration/test_usersync.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ async def test_user_sync(self, ops_test: OpsTest):

ranger_config = {
"charm-function": "usersync",
"ranger-usersync-password": "P@ssw0rd1234",
}

charm = await ops_test.build_charm(".")
Expand Down
11 changes: 8 additions & 3 deletions tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ def test_admin_ready(self):
"OPENSEARCH_PWD": None,
"OPENSEARCH_PORT": None,
"OPENSEARCH_USER": None,
"RANGER_USERSYNC_PWD": "rangerR0cks!",
},
}
},
Expand Down Expand Up @@ -171,6 +172,7 @@ def test_usersync_ready(self):
"startup": "enabled",
"environment": {
"POLICY_MGR_URL": "http://ranger-k8s:6080",
"RANGER_USERSYNC_PWD": "rangerR0cks!",
"SYNC_GROUP_USER_MAP_SYNC_ENABLED": True,
"SYNC_GROUP_SEARCH_ENABLED": True,
"SYNC_GROUP_SEARCH_BASE": "dc=canonical,dc=dev,dc=com",
Expand Down Expand Up @@ -208,10 +210,10 @@ def test_config_changed(self):
simulate_admin_lifecycle(harness)

# Update the config.
self.harness.update_config({"ranger-admin-password": "secure-pass"})
self.harness.update_config({"ranger-admin-password": "s3cure-pass"})

# The new plan reflects the change.
want_admin_password = "secure-pass" # nosec
want_admin_password = "rangerR0cks!" # nosec
got_admin_password = harness.get_container_pebble_plan(
"ranger"
).to_dict()["services"]["ranger"]["environment"]["RANGER_ADMIN_PWD"]
Expand All @@ -221,7 +223,10 @@ def test_config_changed(self):
# The Maintenance Status is set with replan message.
self.assertEqual(
harness.model.unit.status,
MaintenanceStatus("replanning application"),
BlockedStatus(
"value of 'ranger-admin-password' config cannot be changed after deployment. "
"Value should be rangerR0cks!"
),
)

def test_ingress(self):
Expand Down
Loading

0 comments on commit 0f15f1d

Please sign in to comment.