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

karellen / kubernator / 18550821136

16 Oct 2025 05:02AM UTC coverage: 75.193% (-0.2%) from 75.397%
18550821136

push

github

web-flow
Merge pull request #86 from karellen/job_conflict_replacement

Job is partially immutable and needs to be replaced

630 of 992 branches covered (63.51%)

Branch coverage included in aggregate %.

43 of 83 new or added lines in 2 files covered. (51.81%)

15 existing lines in 3 files now uncovered.

2480 of 3144 relevant lines covered (78.88%)

4.73 hits per line

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

87.81
/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
6✔
20
import logging
6✔
21
import os
6✔
22
import tarfile
6✔
23
import tempfile
6✔
24
from pathlib import Path
6✔
25
from shutil import which
6✔
26

27
import yaml
6✔
28

29
from kubernator.api import (KubernatorPlugin, scan_dir,
6✔
30
                            TemplateEngine,
31
                            load_remote_file,
32
                            FileType,
33
                            StripNL,
34
                            Globs,
35
                            get_golang_os,
36
                            get_golang_machine,
37
                            prepend_os_path, jp, load_file)
38
from kubernator.plugins.k8s import api_exc_normalize_body, api_exc_format_body
6✔
39
from kubernator.plugins.k8s_api import K8SResourcePluginMixin
6✔
40

41
logger = logging.getLogger("kubernator.istio")
6✔
42
proc_logger = logger.getChild("proc")
6✔
43
stdout_logger = StripNL(proc_logger.info)
6✔
44
stderr_logger = StripNL(proc_logger.warning)
6✔
45

46
MESH_PILOT_JP = jp('$.meshVersion[?Component="pilot"].Info.version')
6✔
47

48

49
class IstioPlugin(KubernatorPlugin, K8SResourcePluginMixin):
6✔
50
    logger = logger
6✔
51
    _name = "istio"
6✔
52

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

63
        self.istioctl_dir = None
6✔
64

65
        super().__init__()
6✔
66

67
    def register(self, version=None):
6✔
68
        context = self.context
6✔
69
        context.app.register_plugin("kubeconfig")
6✔
70
        context.app.register_plugin("k8s")
6✔
71

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

91
            istioctl_file = str(Path(self.istioctl_dir.name) / "istioctl")
6✔
92
            istio_tar = tarfile.open(istioctl_file_dl)
6✔
93
            istio_tar.extractall(self.istioctl_dir.name)
6✔
94

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

103
            logger.debug("Found istioctl in %r", istioctl_file)
×
104

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

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

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

123
        logger.info("Found Istio client version %s", version)
6✔
124

125
        return version, version_out_js
6✔
126

127
    def set_context(self, context):
6✔
128
        self.context = context
6✔
129

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

134
    def handle_init(self):
6✔
135
        pass
6✔
136

137
    def handle_start(self):
6✔
138
        context = self.context
6✔
139

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

144
        if mesh_versions:
6✔
145
            self.server_version = max(mesh_versions)
6✔
146

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

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

162
        if self.upgrade and (self.client_version >= (1, 24, 0) > self.server_version):
6✔
163
            self.upgrade_from_operator = True
6✔
164

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

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

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

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

190
        # Exclude Istio YAMLs from K8S resource loading
191
        context.k8s.default_excludes.add("*.istio.yaml")
6✔
192
        context.k8s.default_excludes.add("*.istio.yml")
6✔
193

194
    def handle_before_dir(self, cwd: Path):
6✔
195
        context = self.context
6✔
196

197
        context.istio.default_includes = Globs(context.istio.default_includes)
6✔
198
        context.istio.default_excludes = Globs(context.istio.default_excludes)
6✔
199
        context.istio.includes = Globs(context.istio.default_includes)
6✔
200
        context.istio.excludes = Globs(context.istio.default_excludes)
6✔
201

202
        # Exclude Istio YAMLs from K8S resource loading
203
        context.k8s.excludes.add("*.istio.yaml")
6✔
204
        context.k8s.excludes.add("*.istio.yml")
6✔
205

206
    def handle_after_dir(self, cwd: Path):
6✔
207
        context = self.context
6✔
208
        istio = context.istio
6✔
209

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

215
            manifests = load_file(logger, p, FileType.YAML, display_p,
6✔
216
                                  self.template_engine,
217
                                  {"ktor": context})
218

219
            self.add_resources(manifests, display_p)
6✔
220

221
    def handle_apply(self):
6✔
222
        context = self.context
6✔
223

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

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

238
                dry_run = context.app.args.dry_run
6✔
239

240
                if self.provision_operator:
6✔
241
                    self._create_istio_system_ns(True)
6✔
242
                    self._operator_init(operators_file, True)
6✔
243

244
                    if not dry_run:
6!
245
                        self._create_istio_system_ns(False)
6✔
246
                        self._operator_init(operators_file, False)
6✔
247
                elif self.install:
6✔
248
                    self._install(operators_file, True)
6✔
249

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

266
                    _upgrade(True)
6✔
267
                    if not dry_run:
6!
268
                        _upgrade(False)
6✔
269

270
    def _delete_resource_internal(self, manifest, dry_run=True, missing_ok=False):
6✔
271
        from kubernetes import client
6✔
272
        from kubernetes.client.rest import ApiException
6✔
273

274
        context = self.context
6✔
275
        k8s_client = context.k8s.client
6✔
276

277
        res = self._create_resource(manifest)
6✔
278
        res.rdef.populate_api(client, k8s_client)
6✔
279
        try:
6✔
280
            res.delete(dry_run=dry_run)
6✔
281
        except ApiException as e:
6✔
282
            api_exc_normalize_body(e)
6✔
283
            try:
6✔
284
                skip = False
6✔
285
                if e.status == 404 and missing_ok:
6!
286
                    skip = True
6✔
287
                if not skip:
6!
NEW
288
                    raise
×
NEW
289
            except ApiException as e:
×
NEW
290
                api_exc_format_body(e)
×
UNCOV
291
                raise
×
292

293
        return res
6✔
294

295
    def _create_resource_internal(self, manifest, dry_run=True, exists_ok=False):
6✔
296
        from kubernetes import client
6✔
297
        from kubernetes.client.rest import ApiException
6✔
298

299
        context = self.context
6✔
300
        k8s_client = context.k8s.client
6✔
301

302
        res = self._create_resource(manifest)
6✔
303
        res.rdef.populate_api(client, k8s_client)
6✔
304
        try:
6✔
305
            res.create(dry_run=dry_run)
6✔
306
        except ApiException as e:
6✔
307
            skip = False
6✔
308
            api_exc_normalize_body(e)
6✔
309
            try:
6✔
310
                if e.status == 409:
6!
311
                    status = e.body
6✔
312
                    if status["reason"] == "AlreadyExists" and exists_ok:
6!
313
                        skip = True
6✔
314
                if not skip:
6!
NEW
315
                    raise
×
NEW
316
            except ApiException as e:
×
NEW
317
                api_exc_format_body(e)
×
UNCOV
318
                raise
×
319

320
        return res
6✔
321

322
    def _install(self, operators_file, dry_run):
6✔
323
        context = self.context
6✔
324
        status_details = " (dry run)" if dry_run else ""
6✔
325

326
        logger.info("Running Istio install%s", status_details)
6✔
327
        istio_install_cmd = context.istio.stanza() + ["install", "-f", operators_file.name, "-y", "--verify"]
6✔
328
        context.app.run(istio_install_cmd + (["--dry-run"] if dry_run else []),
6✔
329
                        stdout_logger,
330
                        stderr_logger).wait()
331

332
    def _upgrade(self, operators_file, dry_run):
6✔
333
        context = self.context
6✔
334
        status_details = " (dry run)" if dry_run else ""
6✔
335

336
        logger.info("Running Istio upgrade%s", status_details)
6✔
337
        istio_upgrade_cmd = context.istio.stanza() + ["upgrade", "-f", operators_file.name, "-y", "--verify"]
6✔
338
        context.app.run(istio_upgrade_cmd + (["--dry-run"] if dry_run else []),
6✔
339
                        stdout_logger,
340
                        stderr_logger).wait()
341

342
    def _create_istio_system_ns(self, dry_run):
6✔
343
        status_details = " (dry run)" if dry_run else ""
6✔
344
        logger.info("Creating istio-system namespace%s", status_details)
6✔
345
        self._create_resource_internal({"apiVersion": "v1",
6✔
346
                                        "kind": "Namespace",
347
                                        "metadata": {
348
                                            "labels": {
349
                                                "istio-injection": "disabled"
350
                                            },
351
                                            "name": "istio-system"
352
                                        }
353
                                        },
354
                                       dry_run=dry_run,
355
                                       exists_ok=True
356
                                       )
357

358
    def _operator_init(self, operators_file, dry_run):
6✔
359
        context = self.context
6✔
360
        status_details = " (dry run)" if dry_run else ""
6✔
361

362
        logger.info("Running Istio operator init%s", status_details)
6✔
363
        istio_operator_init = context.istio.stanza() + ["operator", "init", "-f", operators_file.name]
6✔
364
        context.app.run(istio_operator_init + (["--dry-run"] if dry_run else []),
6✔
365
                        stdout_logger,
366
                        stderr_logger).wait()
367

368
    def __repr__(self):
6✔
369
        return "Istio Plugin"
6✔
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