Repository: canonical/charmcraft

Issue:
Enforce deprecation DN03: Bases configuration is now required
See [the Deprecations page](https://juju.is/docs/sdk/charmcraft-deprecations) for more info.

Discussion:

Patch:
diff --git a/charmcraft/config.py b/charmcraft/config.py
index e50ec066..3f53d864 100644
--- a/charmcraft/config.py
+++ b/charmcraft/config.py
@@ -73,7 +73,6 @@ from typing import Any, Dict, List, Optional, Tuple
 import pydantic
 from craft_cli import CraftError
 
-from charmcraft.deprecations import notify_deprecation
 from charmcraft.env import (
     get_managed_environment_project_path,
     is_charmcraft_running_in_managed_mode,
@@ -300,14 +299,7 @@ class Config(ModelConfigDefaults, validate_all=False):
     type: str
     charmhub: CharmhubConfig = CharmhubConfig()
     parts: Optional[Dict[str, Any]]
-    bases: List[BasesConfiguration] = [
-        BasesConfiguration(
-            **{
-                "build-on": [Base(name="ubuntu", channel="20.04")],
-                "run-on": [Base(name="ubuntu", channel="20.04")],
-            }
-        )
-    ]
+    bases: Optional[List[BasesConfiguration]]
     analysis: AnalysisConfig = AnalysisConfig()
 
     project: Project
@@ -404,9 +396,9 @@ class Config(ModelConfigDefaults, validate_all=False):
             bases = obj.get("bases")
             if bases is None:
                 # "type" is accessed with get because this code happens before
-                # pydantic actually validating that type is present
+                # pydantic actually validating that the key is present
                 if obj.get("type") == "charm":
-                    notify_deprecation("dn03")
+                    raise CraftError("The field 'bases' is mandatory when type=charm")
                 # Set default bases to Ubuntu 20.04 to match strict snap's
                 # effective behavior.
                 bases = [
diff --git a/charmcraft/deprecations.py b/charmcraft/deprecations.py
index 53c52ae3..000d316c 100644
--- a/charmcraft/deprecations.py
+++ b/charmcraft/deprecations.py
@@ -1,4 +1,4 @@
-# Copyright 2021 Canonical Ltd.
+# Copyright 2021-2022 Canonical Ltd.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -32,7 +32,6 @@ from charmcraft.env import is_charmcraft_running_in_managed_mode
 # the message to show for each deprecation ID (this needs to be in sync with the
 # documentation)
 _DEPRECATION_MESSAGES = {
-    "dn03": "Bases configuration is now required.",
     "dn04": "Use 'charm-entrypoint' in charmcraft.yaml parts to define the entry point.",
     "dn05": "Use 'charm-requirements' in charmcraft.yaml parts to define requirements.",
     "dn06": "The build command is deprecated, use 'pack' instead.",


Test patch:
diff --git a/tests/commands/test_build.py b/tests/commands/test_build.py
index 992797b5..897c066c 100644
--- a/tests/commands/test_build.py
+++ b/tests/commands/test_build.py
@@ -81,7 +81,7 @@ def get_builder(
 
 
 @pytest.fixture
-def basic_project(tmp_path, monkeypatch):
+def basic_project(tmp_path, monkeypatch, create_config):
     """Create a basic Charmcraft project."""
     build_dir = tmp_path / BUILD_DIRNAME
     build_dir.mkdir()
@@ -123,6 +123,18 @@ def basic_project(tmp_path, monkeypatch):
     readme = tmp_path / "README.md"
     readme.write_text("README content")
 
+    # the config
+    host_base = get_host_as_base()
+    create_config(
+        f"""
+        type: charm
+        bases:
+          - name: {host_base.name}
+            channel: "{host_base.channel}"
+            architectures: {host_base.architectures!r}
+        """
+    )
+
     # paths are relative, make all tests to run in the project's directory
     monkeypatch.chdir(tmp_path)
 
@@ -522,7 +534,9 @@ def test_build_basic_complete_structure(basic_project, caplog, monkeypatch, conf
     ), patch("charmcraft.env.get_managed_environment_home_path", return_value=tmp_path / "root"):
         zipnames = builder.run()
 
-    assert zipnames == [f"name-from-metadata_ubuntu-20.04-{host_arch}.charm"]
+    assert zipnames == [
+        f"name-from-metadata_{host_base.name}-{host_base.channel}-{host_arch}.charm"
+    ]
 
     # check all is properly inside the zip
     # contents!), and all relative to build dir
@@ -604,7 +618,11 @@ def test_build_checks_provider(basic_project, mock_provider):
     config = load(basic_project)
     builder = get_builder(config)
 
-    builder.run()
+    try:
+        builder.run()
+    except CraftError:
+        # 'No suitable 'build-on' environment...' error will be raised on some test platforms
+        pass
 
     mock_provider.ensure_provider_is_available.assert_called_once()
 
@@ -1337,12 +1355,13 @@ def test_build_package_name(tmp_path, config):
     assert zipname == "name-from-metadata_xname-xchannel-xarch1.charm"
 
 
-def test_build_with_entrypoint_argument_issues_dn04(basic_project, emitter, monkeypatch):
+def test_build_with_entrypoint_argument_issues_dn04(basic_project, emitter):
     """Test cases for base-index parameter."""
     config = load(basic_project)
     builder = get_builder(config)
 
-    builder.run()
+    with patch("charmcraft.commands.build.Builder.build_charm"):
+        builder.run(destructive_mode=True)
 
     emitter.assert_message(
         "DEPRECATED: Use 'charm-entrypoint' in charmcraft.yaml parts to define the entry point.",
@@ -1742,12 +1761,13 @@ def test_build_postlifecycle_validation_entrypoint_nonexec(basic_project):
     assert str(cm.value) == f"Charm entry point must be executable: {str(entrypoint)!r}"
 
 
-def test_build_with_requirement_argment_issues_dn05(basic_project, emitter, monkeypatch):
+def test_build_with_requirement_argment_issues_dn05(basic_project, emitter):
     """Test cases for base-index parameter."""
     config = load(basic_project)
     builder = get_builder(config, entrypoint=None, requirement=["reqs.txt"])
 
-    builder.run()
+    with patch("charmcraft.commands.build.Builder.build_charm"):
+        builder.run(destructive_mode=True)
 
     emitter.assert_message(
         "DEPRECATED: Use 'charm-requirements' in charmcraft.yaml parts to define requirements.",
diff --git a/tests/conftest.py b/tests/conftest.py
index edebb5ed..99700893 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -29,7 +29,8 @@ from craft_providers import Executor
 
 from charmcraft import config as config_module
 from charmcraft import deprecations, parts
-from charmcraft.config import Base
+from charmcraft.bases import get_host_as_base
+from charmcraft.config import Base, BasesConfiguration
 from charmcraft.providers import Provider
 
 
@@ -67,7 +68,8 @@ def config(tmp_path):
         config_provided=True,
     )
 
-    return TestConfig(type="charm", project=project)
+    base = BasesConfiguration(**{"build-on": [get_host_as_base()], "run-on": [get_host_as_base()]})
+    return TestConfig(type="charm", bases=[base], project=project)
 
 
 @pytest.fixture
@@ -162,9 +164,23 @@ def fake_provider(mock_instance, monkeypatch):
 
 @pytest.fixture
 def create_config(tmp_path):
-    """Helper to create a config file in disk."""
+    """Helper to create a config file in disk.
 
-    def create_config(text):
+    If content is not given, create a minimum valid file.
+    """
+
+    def create_config(text=None):
+        if text is None:
+            text = """
+                type: charm
+                bases:
+                  - build-on:
+                    - name: test-build-name
+                      channel: test-build-channel
+                    run-on:
+                    - name: test-build-name
+                      channel: test-build-channel
+            """
         test_file = tmp_path / "charmcraft.yaml"
         test_file.write_text(text)
         return tmp_path
diff --git a/tests/test_config.py b/tests/test_config.py
index 55508f32..6cb8ca9b 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -1,4 +1,4 @@
-# Copyright 2020-2021 Canonical Ltd.
+# Copyright 2020-2022 Canonical Ltd.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -34,11 +34,7 @@ from charmcraft.utils import get_host_architecture
 
 def test_load_current_directory(create_config, monkeypatch):
     """Init the config using charmcraft.yaml in current directory."""
-    tmp_path = create_config(
-        """
-        type: charm
-    """
-    )
+    tmp_path = create_config()
     monkeypatch.chdir(tmp_path)
     fake_utcnow = datetime.datetime(1970, 1, 1, 0, 0, 2, tzinfo=datetime.timezone.utc)
     with patch("datetime.datetime") as mock:
@@ -66,11 +62,7 @@ def test_load_managed_mode_directory(create_config, monkeypatch, tmp_path):
 
 def test_load_specific_directory_ok(create_config):
     """Init the config using charmcraft.yaml in a specific directory."""
-    tmp_path = create_config(
-        """
-        type: charm
-    """
-    )
+    tmp_path = create_config()
     config = load(tmp_path)
     assert config.type == "charm"
     assert config.project.dirpath == tmp_path
@@ -93,11 +85,7 @@ def test_load_optional_charmcraft_bad_directory(tmp_path):
 
 def test_load_specific_directory_resolved(create_config, monkeypatch):
     """Ensure that the given directory is resolved to always show the whole path."""
-    tmp_path = create_config(
-        """
-        type: charm
-    """
-    )
+    tmp_path = create_config()
     # change to some dir, and reference the config dir relatively
     subdir = tmp_path / "subdir"
     subdir.mkdir()
@@ -111,11 +99,7 @@ def test_load_specific_directory_resolved(create_config, monkeypatch):
 @pytest.mark.skipif(sys.platform == "win32", reason="Windows not [yet] supported")
 def test_load_specific_directory_expanded(create_config, monkeypatch):
     """Ensure that the given directory is user-expanded."""
-    tmp_path = create_config(
-        """
-        type: charm
-    """
-    )
+    tmp_path = create_config()
     # fake HOME so the '~' indication is verified to work
     monkeypatch.setitem(os.environ, "HOME", str(tmp_path))
     config = load("~")
@@ -161,6 +145,13 @@ def test_schema_type_missing(create_config, check_schema_error):
         """
         charmhub:
             api-url: https://www.canonical.com
+        bases:
+          - build-on:
+            - name: test-build-name
+              channel: test-build-channel
+            run-on:
+            - name: test-build-name
+              channel: test-build-channel
     """
     )
     check_schema_error(
@@ -202,7 +193,7 @@ def test_schema_charmhub_api_url_bad_type(create_config, check_schema_error):
     """Schema validation, charmhub.api-url must be a string."""
     create_config(
         """
-        type: charm  # mandatory
+        type: bundle  # mandatory
         charmhub:
             api-url: 33
     """
@@ -218,7 +209,7 @@ def test_schema_charmhub_api_url_bad_format(create_config, check_schema_error):
     """Schema validation, charmhub.api-url must be a full URL."""
     create_config(
         """
-        type: charm  # mandatory
+        type: bundle  # mandatory
         charmhub:
             api-url: stuff.com
     """
@@ -234,7 +225,7 @@ def test_schema_charmhub_storage_url_bad_type(create_config, check_schema_error)
     """Schema validation, charmhub.storage-url must be a string."""
     create_config(
         """
-        type: charm  # mandatory
+        type: bundle  # mandatory
         charmhub:
             storage-url: 33
     """
@@ -250,7 +241,7 @@ def test_schema_charmhub_storage_url_bad_format(create_config, check_schema_erro
     """Schema validation, charmhub.storage-url must be a full URL."""
     create_config(
         """
-        type: charm  # mandatory
+        type: bundle  # mandatory
         charmhub:
             storage-url: stuff.com
     """
@@ -266,7 +257,7 @@ def test_schema_charmhub_registry_url_bad_type(create_config, check_schema_error
     """Schema validation, charmhub.registry-url must be a string."""
     create_config(
         """
-        type: charm  # mandatory
+        type: bundle  # mandatory
         charmhub:
             registry-url: 33
     """
@@ -282,7 +273,7 @@ def test_schema_charmhub_registry_url_bad_format(create_config, check_schema_err
     """Schema validation, charmhub.registry-url must be a full URL."""
     create_config(
         """
-        type: charm  # mandatory
+        type: bundle  # mandatory
         charmhub:
             registry-url: stuff.com
     """
@@ -315,7 +306,7 @@ def test_schema_basicprime_bad_init_structure(create_config, check_schema_error)
     """Schema validation, basic prime with bad parts."""
     create_config(
         """
-        type: charm  # mandatory
+        type: bundle  # mandatory
         parts: ['foo', 'bar']
     """
     )
@@ -331,7 +322,7 @@ def test_schema_basicprime_bad_bundle_structure(create_config, check_schema_erro
     """Schema validation, basic prime with bad bundle."""
     create_config(
         """
-        type: charm  # mandatory
+        type: bundle  # mandatory
         parts:
             charm: ['foo', 'bar']
     """
@@ -347,7 +338,7 @@ def test_schema_basicprime_bad_prime_structure(create_config, check_schema_error
     """Schema validation, basic prime with bad prime."""
     create_config(
         """
-        type: charm  # mandatory
+        type: bundle  # mandatory
         parts:
             charm:
                 prime: foo
@@ -364,7 +355,7 @@ def test_schema_basicprime_bad_prime_type(create_config, check_schema_error):
     """Schema validation, basic prime with a prime holding not strings."""
     create_config(
         """
-        type: charm  # mandatory
+        type: bundle  # mandatory
         parts:
             charm:
                 prime: [{}, 'foo']
@@ -381,7 +372,7 @@ def test_schema_basicprime_bad_prime_type_empty(create_config, check_schema_erro
     """Schema validation, basic prime with a prime holding not strings."""
     create_config(
         """
-        type: charm  # mandatory
+        type: bundle  # mandatory
         parts:
             charm:
                 prime: ['', 'foo']
@@ -396,7 +387,7 @@ def test_schema_basicprime_bad_content_format(create_config, check_schema_error)
     """Schema validation, basic prime with a prime holding not strings."""
     create_config(
         """
-        type: charm  # mandatory
+        type: bundle  # mandatory
         parts:
             charm:
                 prime: ['/bar/foo', 'foo']
@@ -416,7 +407,7 @@ def test_schema_additional_part(create_config, check_schema_error):
     """Schema validation, basic prime with bad part."""
     create_config(
         """
-        type: charm  # mandatory
+        type: bundle  # mandatory
         parts:
             other-part: 1
     """
@@ -433,6 +424,13 @@ def test_schema_other_charm_part_no_source(create_config, check_schema_error):
     create_config(
         """
         type: charm  # mandatory
+        bases:  # mandatory
+          - build-on:
+            - name: test-build-name
+              channel: test-build-channel
+            run-on:
+            - name: test-build-name
+              channel: test-build-channel
         parts:
             other-part:
                 plugin: charm
@@ -476,7 +474,7 @@ def test_charmhub_underscore_in_names(create_config, check_schema_error):
     """Do not support underscore in attributes, only dash."""
     create_config(
         """
-        type: charm  # mandatory
+        type: bundle  # mandatory
         charmhub:
             storage_url: https://server1.com
     """
@@ -491,24 +489,13 @@ def test_charmhub_underscore_in_names(create_config, check_schema_error):
 # -- tests for bases
 
 
-def test_no_bases_defaults_to_ubuntu_20_04_with_dn03(emitter, create_config, tmp_path):
+def test_no_bases_for_charms(create_config, check_schema_error):
     create_config(
         """
         type: charm
     """
     )
-
-    config = load(tmp_path)
-
-    assert config.bases == [
-        BasesConfiguration(
-            **{
-                "build-on": [Base(name="ubuntu", channel="20.04")],
-                "run-on": [Base(name="ubuntu", channel="20.04")],
-            }
-        )
-    ]
-    emitter.assert_message("DEPRECATED: Bases configuration is now required.", intermediate=True)
+    check_schema_error("The field 'bases' is mandatory when type=charm")
 
 
 def test_no_bases_is_ok_for_bundles(emitter, create_config, tmp_path):
@@ -926,7 +913,7 @@ def test_schema_analysis_missing(create_config, tmp_path):
     """No analysis configuration leads to some defaults in place."""
     create_config(
         """
-        type: charm  # mandatory
+        type: bundle  # mandatory
     """
     )
     config = load(tmp_path)
@@ -938,7 +925,7 @@ def test_schema_analysis_full_struct_just_empty(create_config, tmp_path):
     """Complete analysis structure, empty."""
     create_config(
         """
-        type: charm  # mandatory
+        type: bundle  # mandatory
         analysis:
             ignore:
                 attributes: []
@@ -956,7 +943,7 @@ def test_schema_analysis_ignore_attributes(create_config, tmp_path, create_check
     create_checker("check_ok_2", linters.CheckType.attribute)
     create_config(
         """
-        type: charm  # mandatory
+        type: bundle  # mandatory
         analysis:
             ignore:
                 attributes: [check_ok_1, check_ok_2]
@@ -973,7 +960,7 @@ def test_schema_analysis_ignore_linters(create_config, tmp_path, create_checker)
     create_checker("check_ok_2", linters.CheckType.lint)
     create_config(
         """
-        type: charm  # mandatory
+        type: bundle  # mandatory
         analysis:
             ignore:
                 linters: [check_ok_1, check_ok_2]
@@ -992,7 +979,7 @@ def test_schema_analysis_ignore_attribute_missing(
     create_checker("check_ok_2", linters.CheckType.lint)
     create_config(
         """
-        type: charm  # mandatory
+        type: bundle  # mandatory
         analysis:
             ignore:
                 attributes: [check_ok_1, check_missing]
@@ -1014,7 +1001,7 @@ def test_schema_analysis_ignore_linter_missing(
     create_checker("check_ok_2", linters.CheckType.lint)
     create_config(
         """
-        type: charm  # mandatory
+        type: bundle  # mandatory
         analysis:
             ignore:
                 attributes: [check_ok_1]
diff --git a/tests/test_main.py b/tests/test_main.py
index 40893247..dc08f4fe 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -124,7 +124,7 @@ def test_main_load_config_ok(create_config):
     """Command is properly executed, after loading and receiving the config."""
     tmp_path = create_config(
         """
-        type: charm
+        type: bundle
     """
     )
 
@@ -134,7 +134,7 @@ def test_main_load_config_ok(create_config):
         overview = "test overview"
 
         def run(self, parsed_args):
-            assert self.config.type == "charm"
+            assert self.config.type == "bundle"
 
     with patch("charmcraft.main.COMMAND_GROUPS", [CommandGroup("title", [MyCommand])]):
         retcode = main(["charmcraft", "cmdname", f"--project-dir={tmp_path}"])