Esempio n. 1
0
    def test_translate_resource_kinds(self, k8sconfig):
        """Translate various spellings in `selectors.kinds`"""
        cfg = Config(
            folder=Filepath('/tmp'),
            kubeconfig="",
            kubecontext=None,
            selectors=Selectors(
                kinds={"svc", 'DEPLOYMENT', "Secret"},
                namespaces=['default'],
                labels=["app=morty", "foo=bar"],
            ),
            groupby=GroupBy("", []),
            priorities=["ns", "DEPLOYMENT"],
        )

        # Convert the resource names to their correct K8s kind.
        ret = sq.translate_resource_kinds(cfg, k8sconfig)
        assert ret.selectors.kinds == {"Service", "Deployment", "Secret"}
        assert ret.priorities == ["Namespace", "Deployment"]

        # Add two invalid resource names. This must succeed but return the
        # resource names without having changed them.
        cfg.selectors.kinds.clear()
        cfg.selectors.kinds.update({"invalid", "k8s-resource-kind"})
        cfg.priorities.clear()
        cfg.priorities.extend(["invalid", "k8s-resource-kind"])
        ret = sq.translate_resource_kinds(cfg, k8sconfig)
        assert ret.selectors.kinds == {"invalid", "k8s-resource-kind"}
        assert ret.priorities == ["invalid", "k8s-resource-kind"]
Esempio n. 2
0
    def test_parse_commandline_get_grouping(self, tmp_path):
        """GET supports file hierarchy options."""
        kubeconfig = tmp_path / "kubeconfig.yaml"
        kubeconfig.write_text("")
        base_cmd = ("square.py", "get", "all",
                    "--kubeconfig", str(tmp_path / "kubeconfig.yaml"))

        # ----------------------------------------------------------------------
        # Default file system hierarchy.
        # ----------------------------------------------------------------------
        with mock.patch("sys.argv", base_cmd):
            param = main.parse_commandline_args()
            assert param.groupby is None

        # ----------------------------------------------------------------------
        # User defined file system hierarchy.
        # ----------------------------------------------------------------------
        cmd = ("--groupby", "ns", "kind")
        with mock.patch("sys.argv", base_cmd + cmd):
            param = main.parse_commandline_args()
            assert param.groupby == ["ns", "kind"]

        cfg, err = main.compile_config(param)
        assert not err
        assert cfg.groupby == GroupBy(label="", order=["ns", "kind"])

        # ----------------------------------------------------------------------
        # Include a label into the hierarchy and use "ns" twice.
        # ----------------------------------------------------------------------
        cmd = ("--groupby", "ns", "label=foo", "ns")
        with mock.patch("sys.argv", base_cmd + cmd):
            param = main.parse_commandline_args()
            assert param.groupby == ["ns", "label=foo", "ns"]

        cfg, err = main.compile_config(param)
        assert not err
        assert cfg.groupby == GroupBy(label="foo", order=["ns", "label", "ns"])

        # ----------------------------------------------------------------------
        # The label resource, unlike "ns" or "kind", can only be specified
        # at most once.
        # ----------------------------------------------------------------------
        cmd = ("--groupby", "ns", "label=foo", "label=bar")
        with mock.patch("sys.argv", base_cmd + cmd):
            param = main.parse_commandline_args()
            assert param.groupby == ["ns", "label=foo", "label=bar"]

        expected = Config(
            folder=Filepath(""),
            kubeconfig=Filepath(""),
            kubecontext=None,
            selectors=Selectors(set(), [], []),
            groupby=GroupBy("", []),
            priorities=[],
        )
        assert main.compile_config(param) == (expected, True)
Esempio n. 3
0
    def test_get_resources(self, m_save, m_sync, m_mapi, m_down, m_load,
                           kube_creds, config):
        """Basic test.

        The `get_resource` function is more of a linear script than anything
        else. We merely need to verify it calls the correct functions with the
        correct arguments and aborts if any errors occur.

        """
        k8sconfig: K8sConfig = kube_creds

        # Simulate successful responses from the two auxiliary functions.
        # The `load` function must return empty dicts to ensure the error
        # conditions are properly coded.
        m_load.return_value = ("local_meta", "local_path", False)
        m_down.return_value = ("server", False)
        m_mapi.return_value = ("matched", False)
        m_sync.return_value = ("synced", False)
        m_save.return_value = False

        # `manio.load` must have been called with a wildcard selector to ensure
        # it loads _all_ resources from the local files, even if we want to
        # sync only a subset.
        load_selectors = Selectors(kinds=k8sconfig.kinds,
                                   labels=[],
                                   namespaces=[])

        # Call test function and verify it passed the correct arguments.
        assert sq.get_resources(config) is False
        m_load.assert_called_once_with(config.folder, load_selectors)
        m_down.assert_called_once_with(config, k8sconfig)
        m_mapi.assert_called_once_with(k8sconfig, "local_meta", "server")
        m_sync.assert_called_once_with("local_path", "matched",
                                       config.selectors, config.groupby)
        m_save.assert_called_once_with(config.folder, "synced",
                                       config.priorities)

        # Simulate an error with `manio.save`.
        m_save.return_value = (None, True)
        assert sq.get_resources(config) is True

        # Simulate an error with `manio.sync`.
        m_sync.return_value = (None, True)
        assert sq.get_resources(config) is True

        # Simulate an error in `download_manifests`.
        m_down.return_value = (None, True)
        assert sq.get_resources(config) is True

        # Simulate an error in `load`.
        m_load.return_value = (None, None, True)
        assert sq.get_resources(config) is True
Esempio n. 4
0
def get_resources(cfg: Config) -> bool:
    """Download all K8s manifests and merge them into local files."""
    # Sanity check labels.
    if not all([valid_label(_) for _ in cfg.selectors.labels]):
        logit.error(f"Invalid labels: {cfg.selectors.labels}")
        return True

    try:
        # Create properly configured Requests session to talk to K8s API.
        k8sconfig, err = k8s.cluster_config(cfg.kubeconfig, cfg.kubecontext)
        assert not err

        # Convert "Selectors.kinds" to their canonical names.
        cfg = translate_resource_kinds(cfg, k8sconfig)

        # Use a wildcard Selector to ensure `manio.load` will read _all_ local
        # manifests. This will allow `manio.sync` to modify the ones specified by
        # the `selector` argument only, delete all the local manifests, and then
        # write the new ones. This logic will ensure we never have stale manifests
        # (see `manio.save_files` for details and how `manio.save`, which we call
        # at the end of this function, uses it).
        load_selectors = Selectors(kinds=k8sconfig.kinds,
                                   labels=[],
                                   namespaces=[])

        # Load manifests from local files.
        local_meta, local_path, err = manio.load(cfg.folder, load_selectors)
        assert not err

        # Download manifests from K8s.
        server, err = manio.download(cfg, k8sconfig)
        assert not err

        # Replace the server resources fetched from K8s' preferred endpoint with
        # the one from the endpoint referenced in the local manifest.
        server, err = match_api_version(k8sconfig, local_meta, server)
        assert not err

        # Sync the server manifests into the local manifests. All this happens in
        # memory and no files will be modified here - see `manio.save` in the next step.
        synced_manifests, err = manio.sync(local_path, server, cfg.selectors,
                                           cfg.groupby)
        assert not err

        # Write the new manifest files.
        err = manio.save(cfg.folder, synced_manifests, cfg.priorities)
        assert not err
    except AssertionError:
        return True

    # Success.
    return False
Esempio n. 5
0
 def test_compile_config_missing_k8s_credentials(self, fname_param_config):
     """Gracefully abort if kubeconfig does not exist"""
     _, param, _ = fname_param_config
     param.kubeconfig += "does-not-exist"
     assert main.compile_config(param) == (
         Config(
             folder=Filepath(""),
             kubeconfig=Filepath(""),
             kubecontext=None,
             selectors=Selectors(set(), [], []),
             groupby=GroupBy("", []),
             priorities=[],
         ), True)
Esempio n. 6
0
    def test_expand_all_kinds(self, m_cluster, k8sconfig):
        """Must expand the short names if possible, and leave as is otherwise."""
        m_cluster.side_effect = lambda *args: (k8sconfig, False)

        # Expand an empty list of `Selectors.kinds` to the full range of
        # available K8s resources that Square could manage.
        cfg = Config(
            folder=pathlib.Path('/tmp'),
            kubeconfig="",
            kubecontext=None,
            selectors=Selectors(
                kinds=set(),
                namespaces=['default'],
                labels=["app=morty", "foo=bar"],
            ),
            groupby=GroupBy("", []),
            priorities=("Namespace", "Deployment"),
        )

        # Do nothing if `Selectors.kinds` is non-empty.
        ret, err = main.expand_all_kinds(cfg)
        assert not err and ret.selectors.kinds == set(k8sconfig.kinds)

        cfg = Config(
            folder=pathlib.Path('/tmp'),
            kubeconfig="",
            kubecontext=None,
            selectors=Selectors(
                kinds={"svc", "Deployment"},
                namespaces=['default'],
                labels=["app=morty", "foo=bar"],
            ),
            groupby=GroupBy("", []),
            priorities=("Namespace", "Deployment"),
        )

        # Convert the resource names to their correct K8s kind.
        ret, err = main.expand_all_kinds(cfg)
        assert not err and ret == cfg
Esempio n. 7
0
 def test_config_default(self, tmp_path):
     """Default values for Config."""
     assert Config(folder=tmp_path, kubeconfig=tmp_path,
                   kubecontext="ctx") == Config(
                       folder=tmp_path,
                       kubecontext="ctx",
                       kubeconfig=tmp_path,
                       selectors=Selectors(kinds=set(DEFAULT_PRIORITIES),
                                           namespaces=[],
                                           labels=set()),
                       priorities=list(DEFAULT_PRIORITIES),
                       groupby=GroupBy(label="", order=[]),
                       filters={},
                   )
Esempio n. 8
0
def main():
    # ----------------------------------------------------------------------
    #                                 Setup
    # ----------------------------------------------------------------------
    # Kubernetes credentials.
    kubeconfig, kubecontext = pathlib.Path(os.environ["KUBECONFIG"]), None

    # Optional: Set log level (0 = ERROR, 1 = WARNING, 2 = INFO, 3 = DEBUG).
    square.square.setup_logging(3)

    # Populate the `Config` structure. All main functions expect this.
    config = Config(
        kubeconfig=kubeconfig,
        kubecontext=kubecontext,

        # Store manifests in this folder.
        folder=pathlib.PosixPath('manifests'),

        # Organise the manifests in `folder` above in this order.
        groupby=GroupBy(label="app", order=["ns", "label", "kind"]),

        # Specify the resources to operate on. These ones model the demo setup in
        # the `../integration-test-cluster`.
        selectors=Selectors(
            kinds=["Deployment", "Service", "Namespace"],
            labels=["app=demoapp-1"],
            namespaces=["square-tests-1", "square-tests-2"],
        ),
    )

    # ----------------------------------------------------------------------
    #          Import resources, create a plan and then apply it
    # ----------------------------------------------------------------------
    # Download the manifests into the `folder` defined earlier.
    err = square.get(config)
    assert not err

    # Compute the plan and show it. It will not show any differences because we
    # planned against the resources we just downloaded.
    plan, err = square.plan(config)
    assert not err
    square.show_plan(plan)

    # Apply the plan - will not actually do anything in this case, because the
    # plan is empty.
    err = square.apply_plan(config, plan)
    assert not err
Esempio n. 9
0
    def test_compile_hierarchy_ok(self, fname_param_config):
        """Parse the `--groupby` argument."""
        _, param, _ = fname_param_config

        err_resp = Config(
            folder=Filepath(""),
            kubeconfig=Filepath(""),
            kubecontext=None,
            selectors=Selectors(set(), [], []),
            groupby=GroupBy("", []),
            priorities=[],
        ), True

        # ----------------------------------------------------------------------
        # Default hierarchy.
        # ----------------------------------------------------------------------
        for cmd in ["apply", "get", "plan"]:
            param.parser = cmd
            ret, err = main.compile_config(param)
            assert not err
            assert ret.groupby == GroupBy(label="app", order=["ns", "label", "kind"])
            del cmd, ret, err

        # ----------------------------------------------------------------------
        # User defined hierarchy with a valid label.
        # ----------------------------------------------------------------------
        param.parser = "get"
        param.groupby = ("ns", "kind", "label=app", "ns")
        ret, err = main.compile_config(param)
        assert not err
        assert ret.groupby == GroupBy(label="app", order=["ns", "kind", "label", "ns"])

        # ----------------------------------------------------------------------
        # User defined hierarchy with invalid labels.
        # ----------------------------------------------------------------------
        param.parser = "get"
        invalid_labels = ["label", "label=", "label=foo=bar"]
        for label in invalid_labels:
            param.groupby = ("ns", "kind", label, "ns")
            assert main.compile_config(param) == err_resp

        # ----------------------------------------------------------------------
        # User defined hierarchy with invalid resource types.
        # ----------------------------------------------------------------------
        param.parser = "get"
        param.groupby = ("ns", "unknown")
        assert main.compile_config(param) == err_resp
Esempio n. 10
0
    def test_expand_all_kinds_err_config(self, k8sconfig):
        """Abort if the kubeconfig file does not exist."""
        cfg = Config(
            folder=pathlib.Path('/tmp'),
            kubeconfig=Filepath("/does/not/exist"),
            kubecontext=None,
            selectors=Selectors(
                kinds=set(),
                namespaces=['default'],
                labels=["app=morty", "foo=bar"],
            ),
            groupby=GroupBy("", tuple()),
            priorities=("Namespace", "Deployment"),
        )

        _, err = main.expand_all_kinds(cfg)
        assert err
Esempio n. 11
0
    def test_nonpreferred_api(self, tmp_path):
        """Sync `autoscaling/v1` and `autoscaling/v2beta` at the same time.

        This test is designed to verify that Square will interrogate the
        correct K8s endpoint versions to download the manifest.

        """
        # Only show INFO and above or otherwise this test will produce a
        # humongous amount of useless logs from all the K8s calls.
        square.square.setup_logging(2)

        config = Config(
            folder=tmp_path,
            groupby=GroupBy(label="app", order=[]),
            kubecontext=None,
            kubeconfig=Filepath("/tmp/kubeconfig-kind.yaml"),
            selectors=Selectors(
                kinds={"Namespace", "HorizontalPodAutoscaler"},
                namespaces=["test-hpa"],
                labels=[],
            ),
        )

        # Copy the manifest with the namespace and the two HPAs to the temporary path.
        manifests = list(
            yaml.safe_load_all(open("tests/support/k8s-test-hpa.yaml")))
        man_path = tmp_path / "manifest.yaml"
        man_path.write_text(yaml.dump_all(manifests))
        assert len(manifests) == 3

        # ---------------------------------------------------------------------
        # Deploy the resources: one namespace with two HPAs in it. On will be
        # deployed via `autoscaling/v1` the other via `autoscaling/v2beta2`.
        # ---------------------------------------------------------------------
        sh.kubectl("apply", "--kubeconfig", config.kubeconfig, "-f",
                   str(man_path))

        # ---------------------------------------------------------------------
        # Sync all manifests. This must do nothing. In particular, it must not
        # change the `apiVersion` of either HPA.
        # ---------------------------------------------------------------------
        assert not square.square.get_resources(config)
        assert list(yaml.safe_load_all(man_path.read_text())) == manifests
Esempio n. 12
0
def compile_config(cmdline_param) -> Tuple[Config, bool]:
    """Return `Config` from `cmdline_param`.

    Inputs:
        cmdline_param: SimpleNamespace

    Returns:
        Config, err

    """
    err_resp = Config(
        folder=Filepath(""),
        kubeconfig=Filepath(""),
        kubecontext=None,
        selectors=Selectors(set(), namespaces=[], labels=[]),
        groupby=GroupBy("", []),
        priorities=[],
    ), True

    # Convenience.
    p = cmdline_param

    # Load the default configuration unless the user specified an explicit one.
    if p.configfile:
        logit.info(f"Loading configuration file <{p.configfile}>")
        cfg, err = square.cfgfile.load(p.configfile)

        # Use the folder the `load()` function determined.
        folder = cfg.folder

        # Look for `--kubeconfig`. Default to the value in config file.
        kubeconfig = p.kubeconfig or cfg.kubeconfig
    else:
        # Pick the configuration file, depending on whether the user specified
        # `--no-config`, `--config` and whether a `.square.yaml` file exists.
        default_cfg = DEFAULT_CONFIG_FILE
        dot_square = Filepath(".square.yaml")
        if p.no_config:
            cfg_file = default_cfg
        else:
            cfg_file = dot_square if dot_square.exists() else default_cfg

        logit.info(f"Loading configuration file <{cfg_file}>")
        cfg, err = square.cfgfile.load(cfg_file)

        # Determine which Kubeconfig to use. The order is: `--kubeconfig`,
        # `--config`, `.square`, `KUBECONFIG` environment variable.
        if cfg_file == default_cfg:
            kubeconfig = p.kubeconfig or os.getenv("KUBECONFIG", "")
        else:
            kubeconfig = p.kubeconfig or str(cfg.kubeconfig) or os.getenv("KUBECONFIG", "")  # noqa
        del dot_square, default_cfg

        # Use the current working directory as the folder directory because the
        # user did not specify an explicit configuration file which would
        # contain the desired folder.
        folder = Filepath.cwd()

        # Abort if neither `--kubeconfig` nor KUBECONFIG env var.
        if not kubeconfig:
            logit.error("Must specify a Kubernetes config file.")
            return err_resp
    if err:
        return err_resp

    # Override the folder if user specified "--folder".
    folder = folder if p.folder is None else p.folder

    # Expand the user's home folder, ie if the path contains a "~".
    folder = Filepath(folder).expanduser()
    kubeconfig = Filepath(kubeconfig).expanduser()

    # ------------------------------------------------------------------------
    # GroupBy (determines the folder hierarchy that GET will create).
    # In : `--groupby ns kind label=app`
    # Out: GroupBy(order=["ns", "kind", "label"], label="app")
    # ------------------------------------------------------------------------
    # Unpack the ordering and replace all `label=*` with `label`.
    # NOTE: `p.groups` does not necessarily exist because the option only makes
    #        sense for eg `square GET` and is thus not implemented for `square apply`.
    order = getattr(p, "groupby", None)
    if order is None:
        groupby = cfg.groupby
    else:
        clean_order = [_ if not _.startswith("label") else "label" for _ in order]
        if not set(clean_order).issubset({"ns", "kind", "label"}):
            logit.error("Invalid definition of `groupby`")
            return err_resp

        labels = [_ for _ in order if _.startswith("label")]
        if len(labels) > 1:
            logit.error("Can only specify one `label=<name>` in `--groupby`.")
            return err_resp

        # Unpack the label name from the `--groupby` argument.
        # Example, if user specified `--groupby label=app` then we need to extract
        # the `app` part. This also includes basic sanity checks.
        label_name = ""
        if len(labels) == 1:
            try:
                _, label_name = labels[0].split("=")
                assert len(label_name) > 0
            except (ValueError, AssertionError):
                logit.error(f"Invalid label specification <{labels[0]}>")
                return err_resp

        # Compile the final `GroupBy` structure.
        groupby = GroupBy(order=list(clean_order), label=label_name)
        del order, clean_order, label_name

    # ------------------------------------------------------------------------
    # General: folder, kubeconfig, kubecontext, ...
    # ------------------------------------------------------------------------
    kinds = set(p.kinds) if p.kinds else cfg.selectors.kinds

    # Use the value from the (default) config file unless the user overrode
    # them on the command line.
    kubeconfig = Filepath(kubeconfig)
    kubecontext = p.kubecontext or cfg.kubecontext
    namespaces = cfg.selectors.namespaces if p.namespaces is None else p.namespaces
    sel_labels = cfg.selectors.labels if p.labels is None else p.labels
    priorities = p.priorities or cfg.priorities
    selectors = Selectors(kinds, namespaces, sel_labels)

    # Use filters from (default) config file because they cannot be specified
    # on the command line.
    filters = cfg.filters

    # ------------------------------------------------------------------------
    # Verify inputs.
    # ------------------------------------------------------------------------
    # Abort without credentials.
    if not kubeconfig.exists():
        logit.error(f"Cannot find Kubernetes config file <{kubeconfig}>")
        return err_resp

    # -------------------------------------------------------------------------
    # Assemble the full configuration and return it.
    # -------------------------------------------------------------------------
    cfg = Config(
        folder=folder,
        kubeconfig=kubeconfig,
        kubecontext=kubecontext,
        selectors=selectors,
        groupby=groupby,
        priorities=priorities,
        filters=filters,
    )
    return cfg, False
Esempio n. 13
0
    def test_nondefault_resources(self, tmp_path):
        """Manage an `autoscaling/v1` and `autoscaling/v2beta` at the same time.

        This test is designed to verify that Square will interrogate the
        correct K8s endpoint versions to compute the plan for a resource.

        """
        # Only show INFO and above or otherwise this test will produce a
        # humongous amount of logs from all the K8s calls.
        square.square.setup_logging(2)

        config = Config(
            folder=tmp_path,
            groupby=GroupBy(label="app", order=[]),
            kubecontext=None,
            kubeconfig=Filepath("/tmp/kubeconfig-kind.yaml"),
            selectors=Selectors(kinds={"Namespace", "HorizontalPodAutoscaler"},
                                namespaces=["test-hpa"],
                                labels=[]),
        )

        # Copy the manifest with the namespace and the two HPAs to the temporary path.
        manifests = list(
            yaml.safe_load_all(open("tests/support/k8s-test-hpa.yaml")))
        man_path = tmp_path / "manifest.yaml"
        man_path.write_text(yaml.dump_all(manifests))
        assert len(manifests) == 3

        # ---------------------------------------------------------------------
        # Deploy the resources: one namespace with two HPAs in it. On will be
        # deployed via `autoscaling/v1` the other via `autoscaling/v2beta2`.
        # ---------------------------------------------------------------------
        sh.kubectl("apply", "--kubeconfig", config.kubeconfig, "-f",
                   str(man_path))

        # ---------------------------------------------------------------------
        # The plan must be empty because Square must have interrogated the
        # correct API endpoints for each HPA.
        # ---------------------------------------------------------------------
        plan_1, err = square.square.make_plan(config)
        assert not err
        assert plan_1.create == plan_1.patch == plan_1.delete == []
        del plan_1

        # ---------------------------------------------------------------------
        # Modify the v2beta2 HPA manifest and verify that Square now wants to
        # patch that resource.
        # ---------------------------------------------------------------------
        # Make a change to the manifest and save it.
        tmp_manifests = copy.deepcopy(manifests)
        assert tmp_manifests[2]["apiVersion"] == "autoscaling/v2beta2"
        tmp_manifests[2]["spec"]["metrics"][0]["external"]["metric"][
            "name"] = "foo"
        man_path.write_text(yaml.dump_all(tmp_manifests))

        # The plan must report one patch.
        plan_2, err = square.square.make_plan(config)
        assert not err
        assert plan_2.create == plan_2.delete == [] and len(plan_2.patch) == 1
        assert plan_2.patch[0].meta.name == "hpav2beta2"
        assert plan_2.patch[0].meta.apiVersion == "autoscaling/v2beta2"
        del plan_2

        # ---------------------------------------------------------------------
        # Delete both HPAs with Square.
        # ---------------------------------------------------------------------
        # Keep only the namespace manifest and save the file.
        tmp_manifests = copy.deepcopy(manifests[:1])
        man_path.write_text(yaml.dump_all(tmp_manifests))

        # Square must now want to delete both HPAs.
        plan_3, err = square.square.make_plan(config)
        assert not err
        assert plan_3.create == plan_3.patch == [] and len(plan_3.delete) == 2
        assert {_.meta.name for _ in plan_3.delete} == {"hpav1", "hpav2beta2"}
        assert not square.square.apply_plan(config, plan_3)
        del plan_3

        # ---------------------------------------------------------------------
        # Re-create both HPAs with Square.
        # ---------------------------------------------------------------------
        # Restore the original manifest file.
        man_path.write_text(yaml.dump_all(manifests))

        # Create a plan. That plan must want to restore both HPAs.
        plan_4, err = square.square.make_plan(config)
        assert not err
        assert plan_4.delete == plan_4.patch == [] and len(plan_4.create) == 2
        assert {_.meta.name for _ in plan_4.create} == {"hpav1", "hpav2beta2"}
        assert {_.meta.apiVersion
                for _ in plan_4.create
                } == {"autoscaling/v1", "autoscaling/v2beta2"}
        assert not square.square.apply_plan(config, plan_4)
        del plan_4

        # Apply the plan.
        plan_5, err = square.square.make_plan(config)
        assert not err
        assert plan_5.create == plan_5.patch == plan_5.delete == []
        del plan_5

        # ---------------------------------------------------------------------
        # Verify that a change in the `apiVersion` would mean a patch.
        # ---------------------------------------------------------------------
        # Manually change the API version of one of the HPAs.
        tmp_manifests = copy.deepcopy(manifests)
        assert tmp_manifests[1]["apiVersion"] == "autoscaling/v1"
        tmp_manifests[1]["apiVersion"] = "autoscaling/v2beta2"
        man_path.write_text(yaml.dump_all(tmp_manifests))

        # Square must now produce a single non-empty patch.
        plan_6, err = square.square.make_plan(config)
        assert not err
        assert plan_6.delete == plan_6.create == [] and len(plan_6.patch) == 1
        assert plan_6.patch[0].meta.name == "hpav1"
        assert plan_6.patch[0].meta.apiVersion == "autoscaling/v2beta2"
Esempio n. 14
0
    def test_workflow(self, tmp_path):
        """Delete and restore full namespace with Square.

        We will use `kubectl` to create a new namespace and populate it with
        resources. Then we will use Square to backup it up, delete it and
        finally restore it again.

        """
        # Only show INFO and above or otherwise this test will produce a
        # humongous amount of logs from all the K8s calls.
        square.square.setup_logging(2)

        # Define the resource priority and kinds we have in our workflow
        # manifests. Only target the `test-workflow` labels to avoid problems
        # with non-namespaced resources.
        priorities = (
            "Namespace",
            "Secret",
            "ConfigMap",
            "ClusterRole",
            "ClusterRoleBinding",
            "Role",
            "RoleBinding",
        )
        namespace = "test-workflow"

        config = Config(
            folder=tmp_path / "backup",
            groupby=GroupBy(label="app", order=[]),
            kubecontext=None,
            kubeconfig=Filepath("/tmp/kubeconfig-kind.yaml"),
            priorities=priorities,
            selectors=Selectors(kinds=set(priorities),
                                namespaces=[namespace],
                                labels=["app=test-workflow"]),
        )

        # ---------------------------------------------------------------------
        # Deploy a new namespace with only a few resources. There are no
        # deployments among them to speed up the deletion of the namespace.
        # ---------------------------------------------------------------------
        sh.kubectl("apply", "--kubeconfig", config.kubeconfig, "-f",
                   "tests/support/k8s-test-resources.yaml")

        # ---------------------------------------------------------------------
        # Create a plan for "square-tests". The plan must delete all resources
        # because we have not downloaded any manifests yet.
        # ---------------------------------------------------------------------
        plan_1, err = square.square.make_plan(config)
        assert not err
        assert plan_1.create == plan_1.patch == [] and len(plan_1.delete) > 0

        # ---------------------------------------------------------------------
        # Backup all resources. A plan against that backup must be empty.
        # ---------------------------------------------------------------------
        assert not (config.folder / "_other.yaml").exists()
        err = square.square.get_resources(config)
        assert not err and (config.folder / "_other.yaml").exists()

        plan_2, err = square.square.make_plan(config)
        assert not err
        assert plan_2.create == plan_2.patch == plan_2.delete == []

        # ---------------------------------------------------------------------
        # Apply the first plan to delete all resources including the namespace.
        # ---------------------------------------------------------------------
        assert not square.square.apply_plan(config, plan_1)

        # ---------------------------------------------------------------------
        # Wait until K8s has deleted the namespace.
        # ---------------------------------------------------------------------
        for i in range(120):
            time.sleep(1)
            try:
                sh.kubectl("get", "ns", namespace, "--kubeconfig",
                           config.kubeconfig)
            except sh.ErrorReturnCode_1:
                break
        else:
            assert False, f"Could not delete the namespace <{namespace}> in time"

        # ---------------------------------------------------------------------
        # Use backup manifests to restore the namespace.
        # ---------------------------------------------------------------------
        plan_3, err = square.square.make_plan(config)
        assert not err
        assert plan_3.patch == plan_3.delete == [] and len(plan_3.create) > 0

        # Apply the new plan.
        assert not square.square.apply_plan(config, plan_3)

        plan_4, err = square.square.make_plan(config)
        assert not err
        assert plan_4.create == plan_4.patch == plan_4.delete == []
Esempio n. 15
0
    def test_compile_config_kinds_merge_file(self, config, tmp_path):
        """Merge configuration from file and command line."""
        # Dummy file.
        kubeconfig_override = tmp_path / "kubeconfig"
        kubeconfig_override.write_text("")

        # ---------------------------------------------------------------------
        # Override nothing on the command line except for `kubeconfig` because
        # it must point to a valid file.
        # ---------------------------------------------------------------------
        param = types.SimpleNamespace(
            configfile=Filepath("tests/support/config.yaml"),

            # Must override this and point it to a dummy file or
            # `compile_config` will complain it does not exist.
            kubeconfig=str(kubeconfig_override),

            # User did not specify anything else.
            kubecontext=None,
            folder=None,
            groupby=None,
            kinds=None,
            labels=None,
            namespaces=None,
            priorities=None,
        )

        # Translate command line arguments into `Config`.
        cfg, err = main.compile_config(param)
        assert not err

        assert cfg.folder == Filepath("tests/support").absolute() / "some/path"
        assert cfg.kubeconfig == kubeconfig_override
        assert cfg.kubecontext is None
        assert cfg.priorities == list(DEFAULT_PRIORITIES)
        assert cfg.selectors == Selectors(
            kinds=set(DEFAULT_PRIORITIES),
            namespaces=["default", "kube-system"],
            labels=["app=square"],
        )
        assert cfg.groupby == GroupBy(label="app", order=["ns", "label", "kind"])
        assert set(cfg.filters.keys()) == {
            "_common_", "ConfigMap", "Deployment", "HorizontalPodAutoscaler", "Service"
        }

        # ---------------------------------------------------------------------
        # Override everything on the command line.
        # ---------------------------------------------------------------------
        param = types.SimpleNamespace(
            folder="folder-override",
            kinds=["Deployment", "Namespace"],
            labels=["app=square", "foo=bar"],
            namespaces=["default", "kube-system"],
            kubeconfig=str(kubeconfig_override),
            kubecontext="kubecontext-override",
            groupby=["kind", "label=foo", "ns"],
            priorities=["Namespace", "Deployment"],
            configfile=Filepath("tests/support/config.yaml"),
        )

        # Translate command line arguments into `Config`.
        cfg, err = main.compile_config(param)
        assert not err

        assert cfg.folder == Filepath(param.folder)
        assert cfg.kubeconfig == kubeconfig_override
        assert cfg.kubecontext == "kubecontext-override"
        assert cfg.priorities == ["Namespace", "Deployment"]
        assert cfg.selectors == Selectors(
            kinds={"Namespace", "Deployment"},
            namespaces=["default", "kube-system"],
            labels=["app=square", "foo=bar"],
        )
        assert cfg.groupby == GroupBy(label="foo", order=["kind", "label", "ns"])
        assert set(cfg.filters.keys()) == {
            "_common_", "ConfigMap", "Deployment", "HorizontalPodAutoscaler", "Service"
        }