• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

karellen / kubernator / 12968276086

25 Jan 2025 08:41PM UTC coverage: 74.504%. First build
12968276086

Pull #69

github

web-flow
Merge fcddfa8d6 into 2921fcad4
Pull Request #69: Support istio 1.24+

578 of 933 branches covered (61.95%)

Branch coverage included in aggregate %.

81 of 85 new or added lines in 2 files covered. (95.29%)

2315 of 2950 relevant lines covered (78.47%)

3.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.67
/src/main/python/kubernator/plugins/istio.py
1
# -*- coding: utf-8 -*-
2
#
3
#   Copyright 2020 Express Systems USA, Inc
4
#   Copyright 2021 Karellen, Inc.
5
#
6
#   Licensed under the Apache License, Version 2.0 (the "License");
7
#   you may not use this file except in compliance with the License.
8
#   You may obtain a copy of the License at
9
#
10
#       http://www.apache.org/licenses/LICENSE-2.0
11
#
12
#   Unless required by applicable law or agreed to in writing, software
13
#   distributed under the License is distributed on an "AS IS" BASIS,
14
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
#   See the License for the specific language governing permissions and
16
#   limitations under the License.
17
#
18

19
import json
4✔
20
import logging
4✔
21
import os
4✔
22
import tarfile
4✔
23
import tempfile
4✔
24
from pathlib import Path
4✔
25
from shutil import which
4✔
26

27
import yaml
4✔
28
from kubernator.api import (KubernatorPlugin, scan_dir,
4✔
29
                            TemplateEngine,
30
                            load_remote_file,
31
                            FileType,
32
                            StripNL,
33
                            Globs,
34
                            get_golang_os,
35
                            get_golang_machine,
36
                            prepend_os_path, jp)
37
from kubernator.plugins.k8s_api import K8SResourcePluginMixin
4✔
38

39
logger = logging.getLogger("kubernator.istio")
4✔
40
proc_logger = logger.getChild("proc")
4✔
41
stdout_logger = StripNL(proc_logger.info)
4✔
42
stderr_logger = StripNL(proc_logger.warning)
4✔
43

44
MESH_PILOT_JP = jp('$.meshVersion[?Component="pilot"].Info.version')
4✔
45

46

47
class IstioPlugin(KubernatorPlugin, K8SResourcePluginMixin):
4✔
48
    logger = logger
4✔
49
    _name = "istio"
4✔
50

51
    def __init__(self):
4✔
52
        self.context = None
4✔
53
        self.client_version = None
4✔
54
        self.server_version = None
4✔
55
        self.provision_operator = False
4✔
56
        self.install = False
4✔
57
        self.upgrade = False
4✔
58
        self.upgrade_from_operator = False
4✔
59
        self.template_engine = TemplateEngine(logger)
4✔
60

61
        self.istioctl_dir = None
4✔
62

63
        super().__init__()
4✔
64

65
    def register(self, version=None):
4✔
66
        context = self.context
4✔
67
        context.app.register_plugin("kubeconfig")
4✔
68
        context.app.register_plugin("k8s")
4✔
69

70
        if version:
4!
71
            # Download and use specific version
72
            istioctl_os = get_golang_os()
4✔
73
            if istioctl_os == "darwin":
4!
74
                istioctl_os = "osx"
×
75
                istioctl_machine = get_golang_machine()
×
76
                if istioctl_machine == "amd64":
×
77
                    istioctl_platform = istioctl_os
×
78
                else:
79
                    istioctl_platform = f"{istioctl_os}-{istioctl_machine}"
×
80
            else:
81
                istioctl_platform = f"{istioctl_os}-{get_golang_machine()}"
4✔
82
            istioctl_url = (f"https://github.com/istio/istio/releases/download/{version}/"
4✔
83
                            f"istioctl-{version}-{istioctl_platform}.tar.gz")
84
            istioctl_file_dl, _ = context.app.download_remote_file(logger, istioctl_url, "bin")
4✔
85
            istioctl_file_dl = str(istioctl_file_dl)
4✔
86
            self.istioctl_dir = tempfile.TemporaryDirectory()
4✔
87
            context.app.register_cleanup(self.istioctl_dir)
4✔
88

89
            istioctl_file = str(Path(self.istioctl_dir.name) / "istioctl")
4✔
90
            istio_tar = tarfile.open(istioctl_file_dl)
4✔
91
            istio_tar.extractall(self.istioctl_dir.name)
4✔
92

93
            os.chmod(istioctl_file, 0o500)
4✔
94
            prepend_os_path(self.istioctl_dir.name)
4✔
95
        else:
96
            # Use current version
97
            istioctl_file = which("istioctl")
×
98
            if not istioctl_file:
×
99
                raise RuntimeError("`istioctl` cannot be found and no version has been specified")
×
100

101
            logger.debug("Found istioctl in %r", istioctl_file)
×
102

103
        context.globals.istio = dict(
4✔
104
            default_includes=Globs(["*.istio.yaml", "*.istio.yml"], True),
105
            default_excludes=Globs([".*"], True),
106
            istioctl_file=istioctl_file,
107
            stanza=self.stanza,
108
            test=self.test_istioctl
109
        )
110

111
    def test_istioctl(self):
4✔
112
        context = self.context
4✔
113
        version_out: str = context.app.run_capturing_out(context.istio.stanza() + ["version", "-o", "json"],
4✔
114
                                                         stderr_logger)
115

116
        version_out_js = json.loads(version_out)
4✔
117
        version = version_out_js["clientVersion"]["version"]
4✔
118
        logger.info("Using istioctl %r version %r with stanza %r",
4✔
119
                    self.context.istio.istioctl_file, version, context.istio.stanza())
120

121
        logger.info("Found Istio client version %s", version)
4✔
122

123
        return version, version_out_js
4✔
124

125
    def set_context(self, context):
4✔
126
        self.context = context
4✔
127

128
    def stanza(self):
4✔
129
        context = self.context.istio
4✔
130
        return [context.istioctl_file, f"--kubeconfig={self.context.kubeconfig.kubeconfig}"]
4✔
131

132
    def handle_init(self):
4✔
133
        pass
4✔
134

135
    def handle_start(self):
4✔
136
        context = self.context
4✔
137

138
        version, version_out_js = self.test_istioctl()
4✔
139
        self.client_version = tuple(map(int, version.split(".")))
4✔
140
        mesh_versions = set(tuple(map(int, m.value.split("."))) for m in MESH_PILOT_JP.find(version_out_js))
4✔
141

142
        if mesh_versions:
4✔
143
            self.server_version = max(mesh_versions)
4✔
144

145
        if not self.server_version:
4✔
146
            logger.info("No Istio mesh has been found and it'll be created")
4✔
147
            self.install = True
4✔
148
            self.provision_operator = True
4✔
149
        elif self.server_version != self.client_version:
4!
150
            logger.info("Istio client is version %s while server is up to %s - up/downgrade will be performed",
4✔
151
                        ".".join(map(str, self.client_version)),
152
                        ".".join(map(str, self.server_version)))
153
            self.upgrade = True
4✔
154
            self.provision_operator = True
4✔
155

156
        if self.client_version >= (1, 24, 0):
4✔
157
            # No more operator in 1.24.0+
158
            self.provision_operator = False
4✔
159

160
        if self.upgrade and (self.client_version >= (1, 24, 0) > self.server_version):
4✔
161
            self.upgrade_from_operator = True
4✔
162

163
        if self.upgrade and (self.client_version < (1, 24, 0) <= self.server_version):
4✔
164
            raise ValueError(f"Unable to downgrade Istio from {self.server_version} to {self.client_version}")
4✔
165

166
        # Register Istio-related CRDs with K8S
167
        if self.client_version >= (1, 24, 0):
4✔
168
            crd_path = "manifests/charts/base/files/crd-all.gen.yaml"
4✔
169
        else:
170
            crd_path = "manifests/charts/base/crds/crd-all.gen.yaml"
4✔
171
        self.context.k8s.load_remote_crds(
4✔
172
            f"https://raw.githubusercontent.com/istio/istio/{'.'.join(map(str, self.client_version))}/{crd_path}",
173
            "yaml")
174

175
        # This plugin only deals with Istio Operator, so only load that stuff
176
        self.resource_definitions_schema = load_remote_file(logger,
4✔
177
                                                            f"https://raw.githubusercontent.com/kubernetes/kubernetes/"
178
                                                            f"{self.context.k8s.server_git_version}"
179
                                                            f"/api/openapi-spec/swagger.json",
180
                                                            FileType.JSON)
181
        self._populate_resource_definitions()
4✔
182

183
        crd_operator_version = (1, 23, 4) if self.client_version >= (1, 24, 0) else self.client_version
4✔
184
        self.add_remote_crds(
4✔
185
            f"https://raw.githubusercontent.com/istio/istio/{'.'.join(map(str, crd_operator_version))}/"
186
            f"manifests/charts/istio-operator/crds/crd-operator.yaml", FileType.YAML)
187

188
        # Exclude Istio YAMLs from K8S resource loading
189
        context.k8s.default_excludes.add("*.istio.yaml")
4✔
190
        context.k8s.default_excludes.add("*.istio.yml")
4✔
191

192
    def handle_before_dir(self, cwd: Path):
4✔
193
        context = self.context
4✔
194

195
        context.istio.default_includes = Globs(context.istio.default_includes)
4✔
196
        context.istio.default_excludes = Globs(context.istio.default_excludes)
4✔
197
        context.istio.includes = Globs(context.istio.default_includes)
4✔
198
        context.istio.excludes = Globs(context.istio.default_excludes)
4✔
199

200
        # Exclude Istio YAMLs from K8S resource loading
201
        context.k8s.excludes.add("*.istio.yaml")
4✔
202
        context.k8s.excludes.add("*.istio.yml")
4✔
203

204
    def handle_after_dir(self, cwd: Path):
4✔
205
        context = self.context
4✔
206
        istio = context.istio
4✔
207

208
        for f in scan_dir(logger, cwd, lambda d: d.is_file(), istio.excludes, istio.includes):
4✔
209
            p = cwd / f.name
4✔
210
            display_p = context.app.display_path(p)
4✔
211
            logger.info("Adding Istio Operator from %s", display_p)
4✔
212

213
            with open(p, "rt") as file:
4✔
214
                template = self.template_engine.from_string(file.read())
4✔
215

216
            self.add_resources(template.render({"ktor": context}), display_p)
4✔
217

218
    def handle_apply(self):
4✔
219
        context = self.context
4✔
220

221
        if not self.resources:
4✔
222
            logger.info("Skipping Istio as no Operator was processed")
4✔
223
        else:
224
            with tempfile.NamedTemporaryFile(mode="wt", delete=False) as operators_file:
4✔
225
                logger.info("Saving Istio Operators to %s", operators_file.name)
4✔
226
                yaml.safe_dump_all((r.manifest for r in self.resources.values()), operators_file)
4✔
227

228
            if context.app.args.command == "apply":
4!
229
                logger.info("Running Istio precheck")
4✔
230
                context.app.run(context.istio.stanza() + ["x", "precheck"],
4✔
231
                                stdout_logger, stderr_logger).wait()
232
                context.app.run(context.istio.stanza() + ["validate", "-f", operators_file.name],
4✔
233
                                stdout_logger, stderr_logger).wait()
234

235
                dry_run = context.app.args.dry_run
4✔
236

237
                if self.provision_operator:
4✔
238
                    self._create_istio_system_ns(True)
4✔
239
                    self._operator_init(operators_file, True)
4✔
240

241
                    if not dry_run:
4!
242
                        self._create_istio_system_ns(False)
4✔
243
                        self._operator_init(operators_file, False)
4✔
244
                elif self.install:
4✔
245
                    self._install(operators_file, True)
4✔
246

247
                    if not dry_run:
4!
248
                        self._install(operators_file, False)
4✔
249
                elif self.upgrade:
4!
250
                    def _upgrade(dry_run):
4✔
251
                        if self.upgrade_from_operator:
4✔
252
                            # delete deployment -n istio-system istio-operator
253
                            self._delete_resource_internal({"apiVersion": "apps/v1",
4✔
254
                                                            "kind": "Deployment",
255
                                                            "metadata":
256
                                                                {
257
                                                                    "namespace": "istio-system",
258
                                                                    "name": "istio-operator"
259
                                                                }
260
                                                            }, dry_run, True)
261
                        self._upgrade(operators_file, dry_run)
4✔
262

263
                    _upgrade(True)
4✔
264
                    if not dry_run:
4!
265
                        _upgrade(False)
4✔
266

267
    def _delete_resource_internal(self, manifest, dry_run=True, missing_ok=False):
4✔
268
        from kubernetes import client
4✔
269
        from kubernetes.client.rest import ApiException
4✔
270

271
        context = self.context
4✔
272
        k8s_client = context.k8s.client
4✔
273

274
        res = self._create_resource(manifest)
4✔
275
        res.rdef.populate_api(client, k8s_client)
4✔
276
        try:
4✔
277
            res.delete(dry_run=dry_run)
4✔
278
        except ApiException as e:
4✔
279
            skip = False
4✔
280
            if e.status == 404 and missing_ok:
4!
281
                skip = True
4✔
282
            if not skip:
4!
NEW
283
                raise
×
284
        return res
4✔
285

286
    def _create_resource_internal(self, manifest, dry_run=True, exists_ok=False):
4✔
287
        from kubernetes import client
4✔
288
        from kubernetes.client.rest import ApiException
4✔
289

290
        context = self.context
4✔
291
        k8s_client = context.k8s.client
4✔
292

293
        res = self._create_resource(manifest)
4✔
294
        res.rdef.populate_api(client, k8s_client)
4✔
295
        try:
4✔
296
            res.create(dry_run=dry_run)
4✔
297
        except ApiException as e:
4✔
298
            skip = False
4✔
299
            if e.status == 409:
4!
300
                status = json.loads(e.body)
4✔
301
                if status["reason"] == "AlreadyExists" and exists_ok:
4!
302
                    skip = True
4✔
303
            if not skip:
4!
304
                raise
×
305
        return res
4✔
306

307
    def _install(self, operators_file, dry_run):
4✔
308
        context = self.context
4✔
309
        status_details = " (dry run)" if dry_run else ""
4✔
310

311
        logger.info("Running Istio install%s", status_details)
4✔
312
        istio_install_cmd = context.istio.stanza() + ["install", "-f", operators_file.name, "-y", "--verify"]
4✔
313
        context.app.run(istio_install_cmd + (["--dry-run"] if dry_run else []),
4✔
314
                        stdout_logger,
315
                        stderr_logger).wait()
316

317
    def _upgrade(self, operators_file, dry_run):
4✔
318
        context = self.context
4✔
319
        status_details = " (dry run)" if dry_run else ""
4✔
320

321
        logger.info("Running Istio upgrade%s", status_details)
4✔
322
        istio_upgrade_cmd = context.istio.stanza() + ["upgrade", "-f", operators_file.name, "-y", "--verify"]
4✔
323
        context.app.run(istio_upgrade_cmd + (["--dry-run"] if dry_run else []),
4✔
324
                        stdout_logger,
325
                        stderr_logger).wait()
326

327
    def _create_istio_system_ns(self, dry_run):
4✔
328
        status_details = " (dry run)" if dry_run else ""
4✔
329
        logger.info("Creating istio-system namespace%s", status_details)
4✔
330
        self._create_resource_internal({"apiVersion": "v1",
4✔
331
                                        "kind": "Namespace",
332
                                        "metadata": {
333
                                            "labels": {
334
                                                "istio-injection": "disabled"
335
                                            },
336
                                            "name": "istio-system"
337
                                        }
338
                                        },
339
                                       dry_run=dry_run,
340
                                       exists_ok=True
341
                                       )
342

343
    def _operator_init(self, operators_file, dry_run):
4✔
344
        context = self.context
4✔
345
        status_details = " (dry run)" if dry_run else ""
4✔
346

347
        logger.info("Running Istio operator init%s", status_details)
4✔
348
        istio_operator_init = context.istio.stanza() + ["operator", "init", "-f", operators_file.name]
4✔
349
        context.app.run(istio_operator_init + (["--dry-run"] if dry_run else []),
4✔
350
                        stdout_logger,
351
                        stderr_logger).wait()
352

353
    def __repr__(self):
4✔
354
        return "Istio Plugin"
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc