diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index f33e93dc..1188ad43 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -10,12 +10,11 @@ jobs: uses: canonical/operator-workflows/.github/workflows/integration_test.yaml@main secrets: inherit with: - extra-arguments: > - -m "not (requires_secret)" - --openstack-rc ${GITHUB_WORKSPACE}/openrc - --kube-config ${GITHUB_WORKSPACE}/kube-config - --screenshot-dir /tmp - modules: '["test_addon", "test_core", "test_ingress", "test_cos_grafana", "test_cos_loki", "test_cos_prometheus"]' + extra-arguments: >- + --openstack-rc=${GITHUB_WORKSPACE}/openrc + --kube-config=${GITHUB_WORKSPACE}/kube-config + --screenshot-dir=/tmp + modules: '["test_addon", "test_core", "test_external", "test_ingress", "test_cos_grafana", "test_cos_loki", "test_cos_prometheus"]' pre-run-script: | -c "sudo microk8s enable hostpath-storage sudo microk8s kubectl -n kube-system rollout status -w deployment/hostpath-provisioner @@ -24,44 +23,3 @@ jobs: ./tests/integration/pre_run_script.sh" setup-devstack-swift: true trivy-image-config: ./trivy.yaml - - integration-test-with-secrets: - runs-on: ubuntu-latest - name: Integration Test (With Secrets) - steps: - - uses: actions/checkout@v4 - - run: sudo rm -rf /usr/local/lib/android - - name: Setup Devstack Swift - id: setup-devstack-swift - uses: canonical/setup-devstack-swift@v1 - - name: Create OpenStack credential file - run: echo "${{ steps.setup-devstack-swift.outputs.credentials }}" > openrc - - name: Setup operator environment - uses: charmed-kubernetes/actions-operator@main - with: - provider: microk8s - - name: Enable microk8s plugins - run: | - sudo microk8s enable hostpath-storage ingress registry - sudo microk8s kubectl -n kube-system rollout status -w deployment/hostpath-provisioner - sudo microk8s kubectl -n ingress rollout status -w daemonset.apps/nginx-ingress-microk8s-controller - sudo microk8s kubectl -n container-registry rollout status -w deployment.apps/registry - - name: Dump microk8s config - run: sudo microk8s config > kube-config - - name: Install tox - run: python3 -m pip install tox - - name: Build docker image - run: | - docker build -t localhost:32000/wordpress:test -f wordpress.Dockerfile . - docker push localhost:32000/wordpress:test - - name: Run integration tests - run: > - tox -e integration -- - --akismet-api-key ${{ secrets.TEST_AKISMET_API_KEY }} - --openid-username ${{ secrets.TEST_OPENID_USERNAME }} - --openid-password ${{ secrets.TEST_OPENID_PASSWORD }} - --launchpad-team ${{ secrets.TEST_LAUNCHPAD_TEAM }} - --openstack-rc ./openrc - --kube-config ${GITHUB_WORKSPACE}/kube-config - --wordpress-image localhost:32000/wordpress:test - -k test_external diff --git a/.gitignore b/.gitignore index f02053e7..95977754 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ build .tox .coverage __pycache__ +**/*.rock diff --git a/lib/charms/data_platform_libs/v0/data_interfaces.py b/lib/charms/data_platform_libs/v0/data_interfaces.py index 9fa0021e..0bb594e3 100644 --- a/lib/charms/data_platform_libs/v0/data_interfaces.py +++ b/lib/charms/data_platform_libs/v0/data_interfaces.py @@ -298,7 +298,7 @@ def _on_topic_requested(self, event: TopicRequestedEvent): from collections import namedtuple from datetime import datetime from enum import Enum -from typing import Dict, List, Optional, Set, Union +from typing import Callable, Dict, List, Optional, Set, Tuple, Union from ops import JujuVersion, Secret, SecretInfo, SecretNotFoundError from ops.charm import ( @@ -320,7 +320,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 = 18 +LIBPATCH = 21 PYDEPS = ["ops>=2.0.0"] @@ -377,12 +377,32 @@ class SecretsIllegalUpdateError(SecretError): """Secrets aren't yet available for Juju version used.""" -def get_encoded_field(relation, member, field) -> Dict[str, str]: +def get_encoded_dict( + relation: Relation, member: Union[Unit, Application], field: str +) -> Optional[Dict[str, str]]: """Retrieve and decode an encoded field from relation data.""" - return json.loads(relation.data[member].get(field, "{}")) + data = json.loads(relation.data[member].get(field, "{}")) + if isinstance(data, dict): + return data + logger.error("Unexpected datatype for %s instead of dict.", str(data)) + +def get_encoded_list( + relation: Relation, member: Union[Unit, Application], field: str +) -> Optional[List[str]]: + """Retrieve and decode an encoded field from relation data.""" + data = json.loads(relation.data[member].get(field, "[]")) + if isinstance(data, list): + return data + logger.error("Unexpected datatype for %s instead of list.", str(data)) -def set_encoded_field(relation, member, field, value) -> None: + +def set_encoded_field( + relation: Relation, + member: Union[Unit, Application], + field: str, + value: Union[str, list, Dict[str, str]], +) -> None: """Set an encoded field from relation data.""" relation.data[member].update({field: json.dumps(value)}) @@ -399,7 +419,11 @@ def diff(event: RelationChangedEvent, bucket: Union[Unit, Application]) -> Diff: keys from the event relation databag. """ # Retrieve the old data from the data key in the application relation databag. - old_data = get_encoded_field(event.relation, bucket, "data") + old_data = get_encoded_dict(event.relation, bucket, "data") + + if not old_data: + old_data = {} + # Retrieve the new data from the event relation databag. new_data = ( {key: value for key, value in event.relation.data[event.app].items() if key != "data"} @@ -408,12 +432,16 @@ def diff(event: RelationChangedEvent, bucket: Union[Unit, Application]) -> Diff: ) # These are the keys that were added to the databag and triggered this event. - added = new_data.keys() - old_data.keys() + added = new_data.keys() - old_data.keys() # pyright: ignore [reportGeneralTypeIssues] # These are the keys that were removed from the databag and triggered this event. - deleted = old_data.keys() - new_data.keys() + deleted = old_data.keys() - new_data.keys() # pyright: ignore [reportGeneralTypeIssues] # These are the keys that already existed in the databag, # but had their values changed. - changed = {key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key]} + changed = { + key + for key in old_data.keys() & new_data.keys() # pyright: ignore [reportGeneralTypeIssues] + if old_data[key] != new_data[key] # pyright: ignore [reportGeneralTypeIssues] + } # Convert the new_data to a serializable format and save it for a next diff check. set_encoded_field(event.relation, bucket, "data", new_data) @@ -426,6 +454,9 @@ def leader_only(f): def wrapper(self, *args, **kwargs): if not self.local_unit.is_leader(): + logger.error( + "This operation (%s()) can only be performed by the leader unit", f.__name__ + ) return return f(self, *args, **kwargs) @@ -500,9 +531,14 @@ def get_content(self) -> Dict[str, str]: def set_content(self, content: Dict[str, str]) -> None: """Setting cached secret content.""" - if self.meta: + if not self.meta: + return + + if content: self.meta.set_content(content) self._secret_content = content + else: + self.meta.remove_all_revisions() def get_info(self) -> Optional[SecretInfo]: """Wrapper function to apply the corresponding call on the Secret object within CachedSecret if any.""" @@ -587,11 +623,28 @@ def _get_relation_secret( @abstractmethod def _fetch_specific_relation_data( - self, relation, fields: Optional[List[str]] + self, relation: Relation, fields: Optional[List[str]] ) -> Dict[str, str]: """Fetch data available (directily or indirectly -- i.e. secrets) from the relation.""" raise NotImplementedError + @abstractmethod + def _fetch_my_specific_relation_data( + self, relation: Relation, fields: Optional[List[str]] + ) -> Dict[str, str]: + """Fetch data available (directily or indirectly -- i.e. secrets) from the relation for owner/this_app.""" + raise NotImplementedError + + @abstractmethod + def _update_relation_data(self, relation: Relation, data: Dict[str, str]) -> None: + """Update data available (directily or indirectly -- i.e. secrets) from the relation for owner/this_app.""" + raise NotImplementedError + + @abstractmethod + def _delete_relation_data(self, relation: Relation, fields: List[str]) -> None: + """Delete data available (directily or indirectly -- i.e. secrets) from the relation for owner/this_app.""" + raise NotImplementedError + # Internal helper methods @staticmethod @@ -658,6 +711,40 @@ def _group_secret_fields(secret_fields: List[str]) -> Dict[SecretGroup, List[str secret_fieldnames_grouped.setdefault(SecretGroup.EXTRA, []).append(key) return secret_fieldnames_grouped + def _get_group_secret_contents( + self, + relation: Relation, + group: SecretGroup, + secret_fields: Optional[Union[Set[str], List[str]]] = None, + ) -> Dict[str, str]: + """Helper function to retrieve collective, requested contents of a secret.""" + if not secret_fields: + secret_fields = [] + + if (secret := self._get_relation_secret(relation.id, group)) and ( + secret_data := secret.get_content() + ): + return {k: v for k, v in secret_data.items() if k in secret_fields} + return {} + + @staticmethod + def _content_for_secret_group( + content: Dict[str, str], secret_fields: Set[str], group_mapping: SecretGroup + ) -> Dict[str, str]: + """Select : pairs from input, that belong to this particular Secret group.""" + if group_mapping == SecretGroup.EXTRA: + return { + k: v + for k, v in content.items() + if k in secret_fields and k not in SECRET_LABEL_MAP.keys() + } + + return { + k: v + for k, v in content.items() + if k in secret_fields and SECRET_LABEL_MAP.get(k) == group_mapping + } + @juju_secrets_only def _get_relation_secret_data( self, relation_id: int, group_mapping: SecretGroup, relation_name: Optional[str] = None @@ -667,7 +754,111 @@ def _get_relation_secret_data( if secret: return secret.get_content() - # Public methods + # Core operations on Relation Fields manipulations (regardless whether the field is in the databag or in a secret) + # Internal functions to be called directly from transparent public interface functions (+closely related helpers) + + def _process_secret_fields( + self, + relation: Relation, + req_secret_fields: Optional[List[str]], + impacted_rel_fields: List[str], + operation: Callable, + second_chance_as_normal_field: bool = True, + *args, + **kwargs, + ) -> Tuple[Dict[str, str], Set[str]]: + """Isolate target secret fields of manipulation, and execute requested operation by Secret Group.""" + result = {} + normal_fields = set(impacted_rel_fields) + if req_secret_fields and self.secrets_enabled: + normal_fields = normal_fields - set(req_secret_fields) + secret_fields = set(impacted_rel_fields) - set(normal_fields) + + secret_fieldnames_grouped = self._group_secret_fields(list(secret_fields)) + + for group in secret_fieldnames_grouped: + # operation() should return nothing when all goes well + if group_result := operation(relation, group, secret_fields, *args, **kwargs): + result.update(group_result) + elif second_chance_as_normal_field: + # If it wasn't found as a secret, let's give it a 2nd chance as "normal" field + # Needed when Juju3 Requires meets Juju2 Provider + normal_fields |= set(secret_fieldnames_grouped[group]) + return (result, normal_fields) + + def _fetch_relation_data_without_secrets( + self, app: Application, relation: Relation, fields: Optional[List[str]] + ) -> Dict[str, str]: + """Fetching databag contents when no secrets are involved. + + Since the Provider's databag is the only one holding secrest, we can apply + a simplified workflow to read the Require's side's databag. + This is used typically when the Provides side wants to read the Requires side's data, + or when the Requires side may want to read its own data. + """ + if fields: + return {k: relation.data[app][k] for k in fields if k in relation.data[app]} + else: + return dict(relation.data[app]) + + def _fetch_relation_data_with_secrets( + self, + app: Application, + req_secret_fields: Optional[List[str]], + relation: Relation, + fields: Optional[List[str]] = None, + ) -> Dict[str, str]: + """Fetching databag contents when secrets may be involved. + + This function has internal logic to resolve if a requested field may be "hidden" + within a Relation Secret, or directly available as a databag field. Typically + used to read the Provides side's databag (eigher by the Requires side, or by + Provides side itself). + """ + result = {} + normal_fields = [] + + if not fields: + all_fields = list(relation.data[app].keys()) + normal_fields = [field for field in all_fields if not self._is_secret_field(field)] + + # There must have been secrets there + if all_fields != normal_fields and req_secret_fields: + # So we assemble the full fields list (without 'secret-' fields) + fields = normal_fields + req_secret_fields + + if fields: + result, normal_fields = self._process_secret_fields( + relation, req_secret_fields, fields, self._get_group_secret_contents + ) + + # Processing "normal" fields. May include leftover from what we couldn't retrieve as a secret. + # (Typically when Juju3 Requires meets Juju2 Provides) + if normal_fields: + result.update( + self._fetch_relation_data_without_secrets(app, relation, list(normal_fields)) + ) + return result + + def _update_relation_data_without_secrets( + self, app: Application, relation: Relation, data: Dict[str, str] + ): + """Updating databag contents when no secrets are involved.""" + if any(self._is_secret_field(key) for key in data.keys()): + raise SecretsIllegalUpdateError("Can't update secret {key}.") + + if relation: + relation.data[app].update(data) + + def _delete_relation_data_without_secrets( + self, app: Application, relation: Relation, fields: List[str] + ) -> None: + """Remove databag fields 'fields' from Relation.""" + for field in fields: + relation.data[app].pop(field) + + # Public interface methods + # Handling Relation Fields seamlessly, regardless if in databag or a Juju Secret def get_relation(self, relation_name, relation_id) -> Relation: """Safe way of retrieving a relation.""" @@ -716,12 +907,70 @@ def fetch_relation_data( data[relation.id] = self._fetch_specific_relation_data(relation, fields) return data - # Public methods - mandatory override + def fetch_relation_field( + self, relation_id: int, field: str, relation_name: Optional[str] = None + ) -> Optional[str]: + """Get a single field from the relation data.""" + return ( + self.fetch_relation_data([relation_id], [field], relation_name) + .get(relation_id, {}) + .get(field) + ) + + @leader_only + def fetch_my_relation_data( + self, + relation_ids: Optional[List[int]] = None, + fields: Optional[List[str]] = None, + relation_name: Optional[str] = None, + ) -> Optional[Dict[int, Dict[str, str]]]: + """Fetch data of the 'owner' (or 'this app') side of the relation. - @abstractmethod + NOTE: Since only the leader can read the relation's 'this_app'-side + Application databag, the functionality is limited to leaders + """ + if not relation_name: + relation_name = self.relation_name + + relations = [] + if relation_ids: + relations = [ + self.get_relation(relation_name, relation_id) for relation_id in relation_ids + ] + else: + relations = self.relations + + data = {} + for relation in relations: + if not relation_ids or relation.id in relation_ids: + data[relation.id] = self._fetch_my_specific_relation_data(relation, fields) + return data + + @leader_only + def fetch_my_relation_field( + self, relation_id: int, field: str, relation_name: Optional[str] = None + ) -> Optional[str]: + """Get a single field from the relation data -- owner side. + + NOTE: Since only the leader can read the relation's 'this_app'-side + Application databag, the functionality is limited to leaders + """ + if relation_data := self.fetch_my_relation_data([relation_id], [field], relation_name): + return relation_data.get(relation_id, {}).get(field) + + @leader_only def update_relation_data(self, relation_id: int, data: dict) -> None: """Update the data within the relation.""" - raise NotImplementedError + relation_name = self.relation_name + relation = self.get_relation(relation_name, relation_id) + return self._update_relation_data(relation, data) + + @leader_only + def delete_relation_data(self, relation_id: int, fields: List[str]) -> None: + """Remove field from the relation.""" + relation_name = self.relation_name + relation = self.get_relation(relation_name, relation_id) + return self._delete_relation_data(relation, fields) # Base DataProvides and DataRequires @@ -747,36 +996,32 @@ def _diff(self, event: RelationChangedEvent) -> Diff: # Private methods handling secrets - @leader_only @juju_secrets_only def _add_relation_secret( - self, relation_id: int, content: Dict[str, str], group_mapping: SecretGroup + self, relation: Relation, content: Dict[str, str], group_mapping: SecretGroup ) -> Optional[Secret]: """Add a new Juju Secret that will be registered in the relation databag.""" - relation = self.get_relation(self.relation_name, relation_id) - secret_field = self._generate_secret_field_name(group_mapping) if relation.data[self.local_app].get(secret_field): - logging.error("Secret for relation %s already exists, not adding again", relation_id) + logging.error("Secret for relation %s already exists, not adding again", relation.id) return - label = self._generate_secret_label(self.relation_name, relation_id, group_mapping) + label = self._generate_secret_label(self.relation_name, relation.id, group_mapping) secret = self.secrets.add(label, content, relation) # According to lint we may not have a Secret ID if secret.meta and secret.meta.id: relation.data[self.local_app][secret_field] = secret.meta.id - @leader_only @juju_secrets_only def _update_relation_secret( - self, relation_id: int, content: Dict[str, str], group_mapping: SecretGroup + self, relation: Relation, content: Dict[str, str], group_mapping: SecretGroup ): """Update the contents of an existing Juju Secret, referred in the relation databag.""" - secret = self._get_relation_secret(relation_id, group_mapping) + secret = self._get_relation_secret(relation.id, group_mapping) if not secret: - logging.error("Can't update secret for relation %s", relation_id) + logging.error("Can't update secret for relation %s", relation.id) return old_content = secret.get_content() @@ -784,22 +1029,40 @@ def _update_relation_secret( full_content.update(content) secret.set_content(full_content) - @staticmethod - def _secret_content_grouped( - content: Dict[str, str], secret_fields: Set[str], group_mapping: SecretGroup - ) -> Dict[str, str]: - if group_mapping == SecretGroup.EXTRA: - return { - k: v - for k, v in content.items() - if k in secret_fields and k not in SECRET_LABEL_MAP.keys() - } + def _add_or_update_relation_secrets( + self, + relation: Relation, + group: SecretGroup, + secret_fields: Set[str], + data: Dict[str, str], + ) -> None: + """Update contents for Secret group. If the Secret doesn't exist, create it.""" + secret_content = self._content_for_secret_group(data, secret_fields, group) + if self._get_relation_secret(relation.id, group): + self._update_relation_secret(relation, secret_content, group) + else: + self._add_relation_secret(relation, secret_content, group) - return { - k: v - for k, v in content.items() - if k in secret_fields and SECRET_LABEL_MAP.get(k) == group_mapping - } + @juju_secrets_only + def _delete_relation_secret( + self, relation: Relation, group: SecretGroup, secret_fields: List[str], fields: List[str] + ): + """Update the contents of an existing Juju Secret, referred in the relation databag.""" + secret = self._get_relation_secret(relation.id, group) + + if not secret: + logging.error("Can't update secret for relation %s", relation.id) + return + + old_content = secret.get_content() + new_content = copy.deepcopy(old_content) + for field in fields: + new_content.pop(field) + secret.set_content(new_content) + + if not new_content: + field = self._generate_secret_field_name(group) + relation.data[self.local_app].pop(field) # Mandatory internal overrides @@ -823,46 +1086,60 @@ def _get_relation_secret( if secret_uri := relation.data[self.local_app].get(secret_field): return self.secrets.get(label, secret_uri) - def _fetch_specific_relation_data(self, relation, fields: Optional[List[str]]) -> dict: + def _fetch_specific_relation_data( + self, relation: Relation, fields: Optional[List[str]] + ) -> Dict[str, str]: """Fetching relation data for Provides. - NOTE: Since all secret fields are in the Requires side of the databag, we don't need to worry about that + NOTE: Since all secret fields are in the Provides side of the databag, we don't need to worry about that """ if not relation.app: return {} - if fields: - return {k: relation.data[relation.app].get(k) for k in fields} - else: - return relation.data[relation.app] - - # Public methods -- mandatory overrides + return self._fetch_relation_data_without_secrets(relation.app, relation, fields) + + def _fetch_my_specific_relation_data( + self, relation: Relation, fields: Optional[List[str]] + ) -> dict: + """Fetching our own relation data.""" + secret_fields = None + if relation.app: + secret_fields = get_encoded_list(relation, relation.app, REQ_SECRET_FIELDS) + + return self._fetch_relation_data_with_secrets( + self.local_app, + secret_fields, + relation, + fields, + ) - @leader_only - def update_relation_data(self, relation_id: int, fields: Dict[str, str]) -> None: + def _update_relation_data(self, relation: Relation, data: Dict[str, str]) -> None: """Set values for fields not caring whether it's a secret or not.""" - relation = self.get_relation(self.relation_name, relation_id) - - relation_secret_fields = get_encoded_field(relation, relation.app, REQ_SECRET_FIELDS) + req_secret_fields = [] + if relation.app: + req_secret_fields = get_encoded_list(relation, relation.app, REQ_SECRET_FIELDS) + + _, normal_fields = self._process_secret_fields( + relation, + req_secret_fields, + list(data), + self._add_or_update_relation_secrets, + data=data, + ) - normal_fields = list(fields) - if relation_secret_fields and self.secrets_enabled: - normal_fields = set(fields.keys()) - set(relation_secret_fields) - secret_fields = set(fields.keys()) - set(normal_fields) + normal_content = {k: v for k, v in data.items() if k in normal_fields} + self._update_relation_data_without_secrets(self.local_app, relation, normal_content) - secret_fieldnames_grouped = self._group_secret_fields(list(secret_fields)) + def _delete_relation_data(self, relation: Relation, fields: List[str]) -> None: + """Delete fields from the Relation not caring whether it's a secret or not.""" + req_secret_fields = [] + if relation.app: + req_secret_fields = get_encoded_list(relation, relation.app, REQ_SECRET_FIELDS) - for group in secret_fieldnames_grouped: - secret_content = self._secret_content_grouped(fields, secret_fields, group) - if self._get_relation_secret(relation_id, group): - self._update_relation_secret(relation_id, secret_content, group) - else: - self._add_relation_secret(relation_id, secret_content, group) - - normal_content = {k: v for k, v in fields.items() if k in normal_fields} - relation.data[self.local_app].update( # pyright: ignore [reportGeneralTypeIssues] - normal_content + _, normal_fields = self._process_secret_fields( + relation, req_secret_fields, fields, self._delete_relation_secret, fields=fields ) + self._delete_relation_data_without_secrets(self.local_app, relation, list(normal_fields)) # Public methods - "native" @@ -1021,22 +1298,6 @@ def is_resource_created(self, relation_id: Optional[int] = None) -> bool: else False ) - def _retrieve_group_secret_contents( - self, - relation_id, - group: SecretGroup, - secret_fields: Optional[Union[Set[str], List[str]]] = None, - ) -> Dict[str, str]: - """Helper function to retrieve collective, requested contents of a secret.""" - if not secret_fields: - secret_fields = [] - - if (secret := self._get_relation_secret(relation_id, group)) and ( - secret_data := secret.get_content() - ): - return {k: v for k, v in secret_data.items() if k in secret_fields} - return {} - # Event handlers def _on_relation_created_event(self, event: RelationCreatedEvent) -> None: @@ -1070,82 +1331,41 @@ def _get_relation_secret( def _fetch_specific_relation_data( self, relation, fields: Optional[List[str]] = None ) -> Dict[str, str]: + """Fetching Requires data -- that may include secrets.""" if not relation.app: return {} + return self._fetch_relation_data_with_secrets( + relation.app, self.secret_fields, relation, fields + ) - result = {} - - normal_fields = fields - if not normal_fields: - normal_fields = list(relation.data[relation.app].keys()) - - if self.secret_fields and self.secrets_enabled: - if fields: - # Processing from what was requested - normal_fields = set(fields) - set(self.secret_fields) - secret_fields = set(fields) - set(normal_fields) - - secret_fieldnames_grouped = self._group_secret_fields(list(secret_fields)) - - for group in secret_fieldnames_grouped: - if contents := self._retrieve_group_secret_contents( - relation.id, group, secret_fields - ): - result.update(contents) - else: - # If it wasn't found as a secret, let's give it a 2nd chance as "normal" field - normal_fields |= set(secret_fieldnames_grouped[group]) - else: - # Processing from what is given, i.e. retrieving all - normal_fields = [ - f for f in relation.data[relation.app].keys() if not self._is_secret_field(f) - ] - secret_fields = [ - f for f in relation.data[relation.app].keys() if self._is_secret_field(f) - ] - for group in SecretGroup: - result.update( - self._retrieve_group_secret_contents( - relation.id, group, self.secret_fields - ) - ) - - # Processing "normal" fields. May include leftover from what we couldn't retrieve as a secret. - result.update({k: relation.data[relation.app].get(k) for k in normal_fields}) - return result - - # Public methods -- mandatory overrides + def _fetch_my_specific_relation_data(self, relation, fields: Optional[List[str]]) -> dict: + """Fetching our own relation data.""" + return self._fetch_relation_data_without_secrets(self.local_app, relation, fields) - @leader_only - def update_relation_data(self, relation_id: int, data: dict) -> None: + def _update_relation_data(self, relation: Relation, data: dict) -> None: """Updates a set of key-value pairs in the relation. This function writes in the application data bag, therefore, only the leader unit can call it. Args: - relation_id: the identifier for a particular relation. + relation: the particular relation. data: dict containing the key-value pairs that should be updated in the relation. """ - if any(self._is_secret_field(key) for key in data.keys()): - raise SecretsIllegalUpdateError("Requires side can't update secrets.") + return self._update_relation_data_without_secrets(self.local_app, relation, data) - relation = self.charm.model.get_relation(self.relation_name, relation_id) - if relation: - relation.data[self.local_app].update(data) + def _delete_relation_data(self, relation: Relation, fields: List[str]) -> None: + """Deletes a set of fields from the relation. - # "Native" public methods + This function writes in the application data bag, therefore, + only the leader unit can call it. - def fetch_relation_field( - self, relation_id: int, field: str, relation_name: Optional[str] = None - ) -> Optional[str]: - """Get a single field from the relation data.""" - return ( - self.fetch_relation_data([relation_id], [field], relation_name) - .get(relation_id, {}) - .get(field) - ) + Args: + relation: the particular relation. + fields: list containing the field names that should be removed from the relation. + """ + return self._delete_relation_data_without_secrets(self.local_app, relation, fields) # General events @@ -1555,6 +1775,10 @@ def _assign_relation_alias(self, relation_id: int) -> None: if relation: relation.data[self.local_unit].update({"alias": available_aliases[0]}) + # We need to set relation alias also on the application level so, + # it will be accessible in show-unit juju command, executed for a consumer application unit + self.update_relation_data(relation_id, {"alias": available_aliases[0]}) + def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None: """Emit an aliased event to a particular relation if it has an alias. diff --git a/lib/charms/loki_k8s/v0/loki_push_api.py b/lib/charms/loki_k8s/v0/loki_push_api.py index 1547a3b0..9f9372d2 100644 --- a/lib/charms/loki_k8s/v0/loki_push_api.py +++ b/lib/charms/loki_k8s/v0/loki_push_api.py @@ -480,7 +480,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 = 21 +LIBPATCH = 22 logger = logging.getLogger(__name__) @@ -1773,6 +1773,8 @@ def __init__( recursive: bool = False, container_name: str = "", promtail_resource_name: Optional[str] = None, + *, # TODO: In v1, move the star up so everything after 'charm' is a kwarg + insecure_skip_verify: bool = False, ): super().__init__(charm, relation_name, alert_rules_path, recursive) self._charm = charm @@ -1792,6 +1794,7 @@ def __init__( self._is_syslog = enable_syslog self.topology = JujuTopology.from_charm(charm) self._promtail_resource_name = promtail_resource_name or "promtail-bin" + self.insecure_skip_verify = insecure_skip_verify # architecture used for promtail binary arch = platform.processor() @@ -2153,8 +2156,15 @@ def _current_config(self) -> dict: @property def _promtail_config(self) -> dict: - """Generates the config file for Promtail.""" + """Generates the config file for Promtail. + + Reference: https://grafana.com/docs/loki/latest/send-data/promtail/configuration + """ config = {"clients": self._clients_list()} + if self.insecure_skip_verify: + for client in config["clients"]: + client["tls_config"] = {"insecure_skip_verify": True} + config.update(self._server_config()) config.update(self._positions()) config.update(self._scrape_configs()) diff --git a/renovate.json b/renovate.json index 39a2b6e9..25eced57 100644 --- a/renovate.json +++ b/renovate.json @@ -2,5 +2,30 @@ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:base" + ], + "regexManagers": [ + { + "fileMatch": ["(^|/)rockcraft.yaml$"], + "description": "Update base image references", + "matchStringsStrategy": "any", + "matchStrings": ["# renovate: build-base:\\s+(?[^:]*):(?[^\\s@]*)(@(?sha256:[0-9a-f]*))?", + "# renovate: base:\\s+(?[^:]*):(?[^\\s@]*)(@(?sha256:[0-9a-f]*))?"], + "datasourceTemplate": "docker", + "versioningTemplate": "ubuntu" + } + ], + "packageRules": [ + { + "enabled": true, + "matchDatasources": [ + "docker" + ], + "pinDigests": true + }, + { + "matchFiles": ["rockcraft.yaml"], + "matchUpdateTypes": ["major", "minor", "patch"], + "enabled": false + } ] } diff --git a/requirements.txt b/requirements.txt index 85751548..aa477487 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ mysql-connector-python -ops==2.6.0 +ops==2.7.0 requests==2.31.0 diff --git a/src/charm.py b/src/charm.py index 34d9e988..7e135d8e 100755 --- a/src/charm.py +++ b/src/charm.py @@ -27,7 +27,7 @@ from charms.loki_k8s.v0.loki_push_api import LogProxyConsumer from charms.nginx_ingress_integrator.v0.nginx_route import require_nginx_route from charms.prometheus_k8s.v0.prometheus_scrape import MetricsEndpointProvider -from ops.charm import ActionEvent, CharmBase, LeaderElectedEvent, PebbleReadyEvent, StartEvent +from ops.charm import ActionEvent, CharmBase, LeaderElectedEvent, PebbleReadyEvent from ops.framework import EventBase, StoredState from ops.main import main from ops.model import ( @@ -62,8 +62,8 @@ class _ReplicaRelationNotReady(Exception): _WP_CONFIG_PATH = "/var/www/html/wp-config.php" _CONTAINER_NAME = "wordpress" _SERVICE_NAME = "wordpress" - _WORDPRESS_USER = "www-data" - _WORDPRESS_GROUP = "www-data" + _WORDPRESS_USER = "_daemon_" + _WORDPRESS_GROUP = "_daemon_" _WORDPRESS_DB_CHARSET = "utf8mb4" _DATABASE_RELATION_NAME = "database" @@ -147,10 +147,6 @@ def __init__(self, *args, **kwargs): self, relation_name=self._DATABASE_RELATION_NAME, database_name=self.app.name ) - self.state.set_default( - started=False, - ) - self._require_nginx_route() self.metrics_endpoint = MetricsEndpointProvider( self, @@ -169,7 +165,6 @@ def __init__(self, *args, **kwargs): ) self.framework.observe(self.on.leader_elected, self._setup_replica_data) - self.framework.observe(self.on.start, self._on_start) self.framework.observe(self.on.uploads_storage_attached, self._reconciliation) self.framework.observe(self.database.on.database_created, self._reconciliation) self.framework.observe(self.database.on.endpoints_changed, self._reconciliation) @@ -183,10 +178,6 @@ def __init__(self, *args, **kwargs): self._on_apache_prometheus_exporter_pebble_ready, ) - def _on_start(self, _event: StartEvent): - """Record if the start event is emitted.""" - self.state.started = True - def _set_version(self, _: PebbleReadyEvent): """Set WordPress application version to Juju charm's app version status.""" version_result = self._run_wp_cli( diff --git a/wordpress.Dockerfile b/wordpress.Dockerfile deleted file mode 100644 index f8ada686..00000000 --- a/wordpress.Dockerfile +++ /dev/null @@ -1,141 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. -FROM ubuntu:20.04 - -ARG VERSION=5.9.3 -ENV APACHE_CONFDIR=/etc/apache2 -ENV APACHE_ENVVARS=/etc/apache2/envvars - -LABEL maintainer="wordpress-charmers@lists.launchpad.net" - -# Update all packages, remove cruft, install required packages, configure apache -RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections && \ - apt-get update \ - && apt-get --purge autoremove -y \ - && apt-get install -y apache2 \ - bzr \ - curl \ - git \ - libapache2-mod-php \ - libgmp-dev \ - php \ - php-curl \ - php-gd \ - php-gmp \ - php-mysql \ - php-symfony-yaml \ - php-xml \ - pwgen \ - python3 \ - python3-yaml \ - unzip && \ - sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' "$APACHE_ENVVARS" && \ - . "$APACHE_ENVVARS" && \ - for dir in "$APACHE_LOCK_DIR" "$APACHE_RUN_DIR" "$APACHE_LOG_DIR"; \ - do \ - rm -rvf "$dir"; \ - mkdir -p "$dir"; \ - chown "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$dir"; \ - chmod u=rwx,g=rx,o=rx "$dir"; \ - done && \ - ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.log" && \ - chown -R --no-dereference "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$APACHE_LOG_DIR" - -# Configure PHP and apache2 - mod_php requires us to use mpm_prefork -COPY ./files/docker-php.conf $APACHE_CONFDIR/conf-available/docker-php.conf -COPY ./files/docker-php-swift-proxy.conf $APACHE_CONFDIR/conf-available/docker-php-swift-proxy.conf -# Configure apache 2 to enable /server-status endpoint -COPY ./files/apache2.conf $APACHE_CONFDIR/apache2.conf -# To allow logging to container and logfile -COPY ./files/000-default.conf $APACHE_CONFDIR/sites-available/000-default.conf - -RUN a2enconf docker-php && \ - a2dismod mpm_event && \ - a2enmod headers && \ - a2enmod mpm_prefork && \ - a2enmod proxy && \ - a2enmod proxy_http && \ - a2enmod rewrite && \ - a2enmod ssl - -RUN curl -sSOL https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \ - chmod +x wp-cli.phar && \ - mv wp-cli.phar /usr/local/bin/wp && \ - mkdir /var/www/.wp-cli && \ - chown www-data:www-data /var/www/.wp-cli - -RUN chown -R www-data:www-data /var/www/html - -USER www-data:www-data - -WORKDIR /var/www/html - -RUN wp core download --version=${VERSION} - -RUN set -e; \ - cd ./wp-content/plugins; \ - for plugin in \ - 404page \ - all-in-one-event-calendar \ - coschedule-by-todaymade \ - elementor \ - essential-addons-for-elementor-lite \ - favicon-by-realfavicongenerator \ - feedwordpress \ - genesis-columns-advanced \ - line-break-shortcode \ - no-category-base-wpml \ - post-grid \ - powerpress \ - redirection \ - relative-image-urls \ - rel-publisher \ - safe-svg \ - show-current-template \ - simple-301-redirects \ - simple-custom-css \ - so-widgets-bundle \ - svg-support \ - syntaxhighlighter \ - wordpress-importer \ - wp-font-awesome \ - wp-lightbox-2 \ - wp-markdown \ - wp-mastodon-share \ - wp-polls \ - wp-statistics ;\ - do \ - curl -sSL "https://downloads.wordpress.org/plugin/${plugin}.latest-stable.zip" -o "${plugin}.zip"; \ - unzip "${plugin}.zip"; \ - rm "${plugin}.zip"; \ - done; \ - curl -sSL "https://downloads.wordpress.org/plugin/openid.3.5.0.zip" -o "openid.zip"; \ - unzip "openid.zip"; \ - rm "openid.zip"; \ - # Latest YoastSEO does not support 5.9.3 version of WordPress. - curl -sSL "https://downloads.wordpress.org/plugin/wordpress-seo.18.9.zip" -o "wordpress-seo.zip"; \ - unzip "wordpress-seo.zip"; \ - rm "wordpress-seo.zip"; \ - git clone https://git.launchpad.net/~canonical-sysadmins/wordpress-launchpad-integration/+git/wordpress-launchpad-integration wordpress-launchpad-integration; \ - git clone https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/openstack-objectstorage-k8s openstack-objectstorage-k8s; \ - git clone https://git.launchpad.net/~canonical-sysadmins/wordpress-teams-integration/+git/wordpress-teams-integration wordpress-teams-integration; \ - git clone https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-plugin-xubuntu-team-members xubuntu-team-members; \ - rm -rf */.git - -RUN cd ./wp-content/themes && \ - git clone https://git.launchpad.net/~canonical-sysadmins/ubuntu-community-webthemes/+git/light-wordpress-theme light-wordpress-theme && \ - git clone https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-mscom mscom && \ - git clone https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-thematic thematic && \ - git clone https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-twentyeleven twentyeleven && \ - git clone https://git.launchpad.net/~canonical-sysadmins/ubuntu-cloud-website/+git/ubuntu-cloud-website ubuntu-cloud-website && \ - git clone https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-ubuntu-community ubuntu-community && \ - git clone https://git.launchpad.net/~canonical-sysadmins/ubuntu-community-wordpress-theme/+git/ubuntu-community-wordpress-theme ubuntu-community-wordpress-theme && \ - git clone https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-ubuntu-fi ubuntu-fi && \ - git clone https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-ubuntu-light ubuntu-light && \ - git clone https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-ubuntustudio-wp ubuntustudio-wp && \ - git clone https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-launchpad launchpad && \ - git clone https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-xubuntu-website xubuntu-website && \ - bzr branch lp:resource-centre && \ - rm -rf */.git - -EXPOSE 80 diff --git a/files/apache2.conf b/wordpress_rock/files/etc/apache2/apache2.conf similarity index 100% rename from files/apache2.conf rename to wordpress_rock/files/etc/apache2/apache2.conf diff --git a/files/docker-php-swift-proxy.conf b/wordpress_rock/files/etc/apache2/conf-available/docker-php-swift-proxy.conf similarity index 100% rename from files/docker-php-swift-proxy.conf rename to wordpress_rock/files/etc/apache2/conf-available/docker-php-swift-proxy.conf diff --git a/files/docker-php.conf b/wordpress_rock/files/etc/apache2/conf-available/docker-php.conf similarity index 100% rename from files/docker-php.conf rename to wordpress_rock/files/etc/apache2/conf-available/docker-php.conf diff --git a/files/000-default.conf b/wordpress_rock/files/etc/apache2/sites-available/000-default.conf similarity index 100% rename from files/000-default.conf rename to wordpress_rock/files/etc/apache2/sites-available/000-default.conf diff --git a/wordpress_rock/rockcraft.yaml b/wordpress_rock/rockcraft.yaml new file mode 100644 index 00000000..bac10b83 --- /dev/null +++ b/wordpress_rock/rockcraft.yaml @@ -0,0 +1,311 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. + +name: wordpress +summary: Wordpress rock +description: Wordpress OCI image for the Wordpress charm +base: ubuntu:20.04 +run-user: _daemon_ +license: Apache-2.0 +version: "1.0" +platforms: + amd64: +parts: + apache2: + plugin: dump + source: files + build-packages: + - apache2 + - php + - rsync + overlay-packages: + - apache2 + - libapache2-mod-php + - libgmp-dev + - php + - php-curl + - php-gd + - php-gmp + - php-mysql + - php-symfony-yaml + - php-xml + - pwgen + - python3 + - python3-yaml + - ca-certificates + build-environment: + # Required to source $CRAFT_OVERLAY/etc/apache2/envvars + - APACHE_CONFDIR: /etc/apache2 + - IMAGE_RUN_USER: _daemon_ + - IMAGE_RUN_GROUP: _daemon_ + - IMAGE_RUN_USER_ID: 584792 + - IMAGE_RUN_GROUP_ID: 584792 + overlay-script: | + craftctl default + sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' $CRAFT_OVERLAY/etc/apache2/envvars + sed -ri 's/\{APACHE_RUN_(USER|GROUP):=.+\}/\{APACHE_RUN_\1:=_daemon_\}/' $CRAFT_OVERLAY/etc/apache2/envvars + . $CRAFT_OVERLAY/etc/apache2/envvars + for dir in "$CRAFT_OVERLAY$APACHE_LOCK_DIR" "$CRAFT_OVERLAY$APACHE_RUN_DIR" "$CRAFT_OVERLAY$APACHE_LOG_DIR"; + do + rm -rvf "$dir"; + mkdir -p "$dir"; + chown "$IMAGE_RUN_USER_ID:$IMAGE_RUN_GROUP_ID" "$dir"; + chmod u=rwx,g=rx,o=rx "$dir"; + done + chown -R --no-dereference "$IMAGE_RUN_USER_ID:$IMAGE_RUN_GROUP_ID" "$CRAFT_OVERLAY$APACHE_LOG_DIR" + ln -sfT ../../../dev/stdout "$CRAFT_OVERLAY$APACHE_LOG_DIR/other_vhosts_access.log" + rsync -abP $CRAFT_PART_SRC/etc/apache2/ $CRAFT_OVERLAY/etc/apache2 + + # Enable apache2 modules + chroot $CRAFT_OVERLAY /bin/sh -x <<'EOF' + a2enconf docker-php + a2enmod headers + a2enmod mpm_prefork + a2enmod proxy + a2enmod proxy_http + a2enmod rewrite + a2enmod ssl + EOF + wordpress: + after: + - apache2 + plugin: nil + build-environment: + - WP_VERSION: 5.9.3 + build-packages: + - curl + override-build: | + curl -sSL --create-dirs https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -o wp + chmod +x wp + + mkdir -p wordpress_install_dir + (cd wordpress_install_dir; $CRAFT_PART_BUILD/wp core download --version=${WP_VERSION} --allow-root) + + cp -R . $CRAFT_PART_INSTALL + organize: + wordpress_install_dir: /var/www/html + wp: /usr/local/bin/wp + # Wordpress plugins + get-wordpress-plugins: + plugin: nil + after: + - wordpress + build-packages: + - curl + - unzip + build-environment: + - WP_PLUGINS: >- + 404page + all-in-one-event-calendar + coschedule-by-todaymade + elementor + essential-addons-for-elementor-lite + favicon-by-realfavicongenerator + feedwordpress + genesis-columns-advanced + line-break-shortcode + no-category-base-wpml + post-grid + powerpress + redirection + relative-image-urls + rel-publisher + safe-svg + show-current-template + simple-301-redirects + simple-custom-css + so-widgets-bundle + svg-support + syntaxhighlighter + wordpress-importer + wp-font-awesome + wp-lightbox-2 + wp-markdown + wp-mastodon-share + wp-polls + wp-statistics + override-build: | + for plugin in $WP_PLUGINS; + do + curl -sSL "https://downloads.wordpress.org/plugin/${plugin}.latest-stable.zip" -o "${plugin}.zip" + unzip -q "${plugin}.zip" + rm "${plugin}.zip" + done + curl -sSL "https://downloads.wordpress.org/plugin/openid.3.5.0.zip" -o "openid.zip" + unzip -q "openid.zip" + rm "openid.zip" + # Latest YoastSEO does not support 5.9.3 version of WordPress. + curl -sSL "https://downloads.wordpress.org/plugin/wordpress-seo.18.9.zip" -o "wordpress-seo.zip" + unzip -q "wordpress-seo.zip" + rm "wordpress-seo.zip" + cp -R . $CRAFT_PART_INSTALL + organize: + "*": /var/www/html/wp-content/plugins/ + ## Plugins fetched via git + get-wordpress-launchpad-integration: + after: + - get-wordpress-plugins + plugin: dump + source: https://git.launchpad.net/~canonical-sysadmins/wordpress-launchpad-integration/+git/wordpress-launchpad-integration + source-type: git + organize: + "*": /var/www/html/wp-content/plugins/wordpress-launchpad-integration/ + get-wordpress-teams-integration: + after: + - get-wordpress-plugins + plugin: dump + source: https://git.launchpad.net/~canonical-sysadmins/wordpress-teams-integration/+git/wordpress-teams-integration + source-type: git + organize: + "*": /var/www/html/wp-content/plugins/wordpress-teams-integration/ + get-openstack-objectstorage-k8s: + after: + - get-wordpress-plugins + plugin: dump + source: https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/openstack-objectstorage-k8s + source-type: git + organize: + "*": /var/www/html/wp-content/plugins/openstack-objectstorage-k8s/ + get-wp-plugin-xubuntu-team-members: + after: + - get-wordpress-plugins + plugin: dump + source: https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-plugin-xubuntu-team-members + source-type: git + organize: + "*": /var/www/html/wp-content/plugins/xubuntu-team-members/ + # Wordpress themes + get-light-wordpress-theme: + after: + - wordpress + plugin: dump + source: https://git.launchpad.net/~canonical-sysadmins/ubuntu-community-webthemes/+git/light-wordpress-theme + source-type: git + organize: + "*": /var/www/html/wp-content/themes/light-wordpress-theme/ + get-wp-theme-mscom: + after: + - wordpress + plugin: dump + source: https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-mscom + source-type: git + organize: + "*": /var/www/html/wp-content/themes/mscom/ + get-wp-theme-thematic: + after: + - wordpress + plugin: dump + source: https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-thematic + source-type: git + organize: + "*": /var/www/html/wp-content/themes/thematic/ + get-wp-theme-twentyeleven: + after: + - wordpress + plugin: dump + source: https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-twentyeleven + source-type: git + organize: + "*": /var/www/html/wp-content/themes/twentyeleven/ + get-ubuntu-cloud-website: + after: + - wordpress + plugin: dump + source: https://git.launchpad.net/~canonical-sysadmins/ubuntu-cloud-website/+git/ubuntu-cloud-website + source-type: git + organize: + "*": /var/www/html/wp-content/themes/ubuntu-cloud-website/ + get-wp-theme-ubuntu-community: + after: + - wordpress + plugin: dump + source: https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-ubuntu-community + source-type: git + organize: + "*": /var/www/html/wp-content/themes/ubuntu-community/ + get-ubuntu-community-wordpress-theme: + after: + - wordpress + plugin: dump + source: https://git.launchpad.net/~canonical-sysadmins/ubuntu-community-wordpress-theme/+git/ubuntu-community-wordpress-theme + source-type: git + organize: + "*": /var/www/html/wp-content/themes/ubuntu-community-wordpress-theme/ + get-wp-theme-ubuntu-fi: + after: + - wordpress + plugin: dump + source: https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-ubuntu-fi + source-type: git + organize: + "*": /var/www/html/wp-content/themes/ubuntu-fi/ + get-wp-theme-ubuntu-light: + after: + - wordpress + plugin: dump + source: https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-ubuntu-light + source-type: git + organize: + "*": /var/www/html/wp-content/themes/ubuntu-light/ + get-wp-theme-ubuntustudio-wp: + after: + - wordpress + plugin: dump + source: https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-ubuntustudio-wp + source-type: git + organize: + "*": /var/www/html/wp-content/themes/ubuntustudio-wp/ + get-wp-theme-launchpad: + after: + - wordpress + plugin: dump + source: https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-launchpad + source-type: git + organize: + "*": /var/www/html/wp-content/themes/launchpad/ + get-wp-theme-xubuntu-website: + after: + - wordpress + plugin: dump + source: https://git.launchpad.net/~canonical-sysadmins/wordpress/+git/wp-theme-xubuntu-website + source-type: git + organize: + "*": /var/www/html/wp-content/themes/xubuntu-website/ + get-resource-centre: + after: + - wordpress + plugin: nil + build-packages: [bzr] + override-build: | + bzr branch lp:resource-centre + cp -R . $CRAFT_PART_INSTALL + organize: + resource-centre: /var/www/html/wp-content/themes/resource-centre/ + # Post-install configuration + wordpress-configure: + plugin: nil + after: + - get-wordpress-launchpad-integration + - get-wordpress-teams-integration + - get-openstack-objectstorage-k8s + - get-wp-plugin-xubuntu-team-members + - get-light-wordpress-theme + - get-wp-theme-mscom + - get-wp-theme-thematic + - get-wp-theme-twentyeleven + - get-ubuntu-cloud-website + - get-wp-theme-ubuntu-community + - get-wp-theme-ubuntu-fi + - get-wp-theme-ubuntu-light + - get-wp-theme-ubuntustudio-wp + - get-wp-theme-launchpad + - get-wp-theme-xubuntu-website + - get-resource-centre + - get-ubuntu-community-wordpress-theme + build-environment: + - IMAGE_RUN_USER_ID: 584792 + - IMAGE_RUN_GROUP_ID: 584792 + override-prime: | + craftctl default + rm -rf **/.git + chown $IMAGE_RUN_USER_ID:$IMAGE_RUN_GROUP_ID -R --no-dereference "$CRAFT_PRIME/var/www/html"