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

nfalco79 / k8s-provisioning-plugin / #122

01 Nov 2025 08:29PM UTC coverage: 8.077% (-0.1%) from 8.203%
#122

push

nfalco79
Upgrade build script

21 of 260 relevant lines covered (8.08%)

0.08 hits per line

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

3.51
/src/main/java/com/github/nfalco79/jenkins/plugins/k8s/tools/KubectlInstaller.java
1
/*
2
 * Copyright 2023 Nikolas Falco
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the
5
 * "License"); you may not use this file except in compliance
6
 * with the License.  You may obtain a copy of the License at
7
 *
8
 *   http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing,
11
 * software distributed under the License is distributed on an
12
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
13
 * KIND, either express or implied.  See the License for the
14
 * specific language governing permissions and limitations
15
 * under the License.
16
 */
17
package com.github.nfalco79.jenkins.plugins.k8s.tools;
18

19
import java.io.File;
20
import java.io.IOException;
21
import java.io.InputStream;
22
import java.io.OutputStream;
23
import java.net.Proxy;
24
import java.net.URL;
25
import java.net.URLConnection;
26
import java.nio.charset.StandardCharsets;
27
import java.nio.file.Files;
28
import java.nio.file.Path;
29
import java.nio.file.StandardCopyOption;
30
import java.time.LocalDateTime;
31
import java.util.ArrayList;
32
import java.util.List;
33
import java.util.Objects;
34

35
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
36
import org.apache.commons.io.IOUtils;
37
import org.apache.commons.io.input.CountingInputStream;
38
import org.jenkinsci.remoting.util.VersionNumber;
39
import org.kohsuke.stapler.DataBoundConstructor;
40

41
import com.github.nfalco79.jenkins.plugins.k8s.K8sConstants;
42
import com.github.nfalco79.jenkins.plugins.k8s.Messages;
43

44
import hudson.Extension;
45
import hudson.FilePath;
46
import hudson.FilePath.TarCompression;
47
import hudson.Functions;
48
import hudson.ProxyConfiguration;
49
import hudson.model.Node;
50
import hudson.model.TaskListener;
51
import hudson.remoting.VirtualChannel;
52
import hudson.slaves.NodeSpecific;
53
import hudson.tools.DownloadFromUrlInstaller;
54
import hudson.tools.ToolInstallation;
55
import jenkins.MasterToSlaveFileCallable;
56
import jenkins.model.Jenkins;
57
import net.sf.json.JSONArray;
58
import net.sf.json.JSONObject;
59

60
/**
61
 * Download and installs Kubernetes CLI.
62
 *
63
 * @author Nikolas Falco
64
 */
65
public class KubectlInstaller extends DownloadFromUrlInstaller {
66

67
    private static final boolean DISABLE_CACHE = Boolean.getBoolean(KubectlInstaller.class.getName() + ".cache.disable");
×
68

69
    @DataBoundConstructor
70
    public KubectlInstaller(String id) {
71
        super(id);
×
72
    }
×
73

74
    @Override
75
    public FilePath performInstallation(ToolInstallation tool, Node node, TaskListener log) throws IOException, InterruptedException {
76
        FilePath expected = preferredLocation(tool, node);
×
77
        FilePath kubectl = expected.child(K8sConstants.KUBECTL_CMD);
×
78

79
        Installable installable = getInstallable();
×
80
        if (installable == null) {
×
81
            log.getLogger().println("Invalid tool ID " + id);
×
82
            return expected;
×
83
        }
84

85
        if (installable instanceof NodeSpecific specificNode) {
×
86
            installable = (Installable) specificNode.forNode(node, log);
×
87
            kubectl = expected.child(((KubectlInstallable) installable).cmd);
×
88
        }
89

90
        if (!isUpToDate(expected, installable)) {
×
91
            File cache = getLocalCacheFile(installable, node);
×
92
            boolean skipInstall = false;
×
93
            if (!DISABLE_CACHE && cache.exists()) {
×
94
                log.getLogger().println(Messages.Installer_installFromCache(cache, expected, node.getDisplayName()));
×
95
                try {
96
                    restoreCache(expected, cache);
×
97
                    skipInstall = true;
×
98
                } catch (IOException e) {
×
99
                    log.error("Use of caches failed: " + e.getMessage());
×
100
                }
×
101
            }
102
            if (!skipInstall) {
×
103
                // download the single executable file
104
                URL url = new URL(installable.url);
×
105
                URLConnection con = ProxyConfiguration.open(url);
×
106
                con.setIfModifiedSince(expected.child(".timestamp").lastModified());
×
107
                try (InputStream is = con.getInputStream()) {
×
108
                    kubectl.copyFrom(is);
×
109
                }
110
                // leave a record for the next up-to-date check
111
                expected.child(".installedFrom").write(installable.url, "UTF-8");
×
112
                kubectl.act(new ChmodRecAPlusX());
×
113
                if (!DISABLE_CACHE) {
×
114
                    buildCache(expected, cache);
×
115
                }
116
            }
117
        }
118
        return expected;
×
119
    }
120

121
    private void buildCache(FilePath expected, File cache) throws IOException, InterruptedException {
122
        // update the local cache on master
123
        // download to a temporary file and rename it in to handle concurrency and failure correctly,
124
        Path tmp = new File(cache.getPath() + ".tmp").toPath();
×
125
        try {
126
            Path tmpParent = tmp.getParent();
×
127
            if (tmpParent != null) {
×
128
                Files.createDirectories(tmpParent);
×
129
            }
130
            try (OutputStream out = new GzipCompressorOutputStream(Files.newOutputStream(tmp))) {
×
131
                // workaround to not store current folder as root folder in the archive
132
                // this prevent issue when tool name is renamed
133
                expected.tar(out, "**");
×
134
            }
135
            Files.move(tmp, cache.toPath(), StandardCopyOption.REPLACE_EXISTING);
×
136
        } finally {
137
            Files.deleteIfExists(tmp);
×
138
        }
139
    }
×
140

141
    private File getLocalCacheFile(Installable installable, Node node) throws DetectionFailedException {
142
        Platform platform = Platform.of(node);
×
143
        // we store cache as tar.gz to preserve symlink
144
        return new File(Jenkins.get().getRootPath() //
×
145
                .child("caches") //
×
146
                .child("k8s-provisioning") //
×
147
                .child(platform.toString()) //
×
148
                .child(id + ".tar.gz") //
×
149
                .getRemote());
×
150
    }
151

152
    private void restoreCache(FilePath expected, File cache) throws IOException, InterruptedException {
153
        try (InputStream in = cache.toURI().toURL().openStream()) {
×
154
            CountingInputStream cis = new CountingInputStream(in);
×
155
            try {
156
                Objects.requireNonNull(expected).untarFrom(cis, TarCompression.GZIP);
×
157
            } catch (IOException e) {
×
158
                throw new IOException(Messages.Installer_failedToUnpack(cache.toURI().toURL(), cis.getByteCount()), e);
×
159
            }
×
160
        }
161
    }
×
162

163
    @Extension
164
    public static final class DescriptorImpl extends DownloadFromUrlInstaller.DescriptorImpl<KubectlInstaller> {
1✔
165

166
        private List<? extends Installable> installables = new ArrayList<>();
1✔
167
        private LocalDateTime expiryDate = LocalDateTime.now();
1✔
168

169
        @Override
170
        public String getDisplayName() {
171
            return Messages.Installer_displayName();
1✔
172
        }
173

174
        @Override
175
        public boolean isApplicable(Class<? extends ToolInstallation> toolType) {
176
            return toolType == KubectlInstallation.class;
×
177
        }
178

179
        @Override
180
        public List<? extends Installable> getInstallables() throws IOException {
181
            // move this to a crawler at https://github.com/jenkins-infra/crawler
182
            if (expiryDate.isBefore(LocalDateTime.now()) || installables.isEmpty()) {
×
183
                expiryDate = LocalDateTime.now().plusHours(1);
×
184
                Proxy proxy = Proxy.NO_PROXY;
×
185
                ProxyConfiguration proxyCfg = Jenkins.get().getProxy();
×
186
                if (proxyCfg != null) {
×
187
                    proxy = proxyCfg.createProxy("api.github.com");
×
188
                }
189

190
                String uriTemplate = "https://api.github.com/repos/kubernetes/kubernetes/releases?per_page=100&page=";
×
191
                List<KubectlInstallable> ghInstallables = new ArrayList<>();
×
192
                int page = 1;
×
193
                do {
194
                    URL githubURL = new URL(uriTemplate + page);
×
195
                    try (InputStream is = githubURL.openConnection(proxy).getInputStream()) {
×
196
                        JSONArray releases = JSONArray.fromObject(IOUtils.toString(is, StandardCharsets.UTF_8));
×
197
                        if (releases.isEmpty()) {
×
198
                            break;
199
                        }
200
                        releases.forEach(rel -> {
×
201
                            JSONObject release = (JSONObject) rel;
×
202
                            if (!release.getBoolean("prerelease") && !release.getBoolean("draft")) {
×
203
                                String id = release.getString("tag_name");
×
204
                                String name = release.getString("name");
×
205
                                ghInstallables.add(new KubectlInstallable(id, name));
×
206
                                new VersionNumber(name);
×
207
                                ghInstallables.sort((i1, i2) -> new VersionNumber(i2.id).compareTo(new VersionNumber(i1.id)));
×
208
                            }
209
                        });
×
210
                        page += 1;
×
211
                    }
×
212
                } while (true);
×
213
                installables = ghInstallables;
×
214
            }
215
            return installables;
×
216
        }
217

218
    }
219

220
    /**
221
     * Sets execute permission on given file.
222
     */
223
    static class ChmodRecAPlusX extends MasterToSlaveFileCallable<Void> {
×
224
        private static final long serialVersionUID = 1L;
225

226
        @Override
227
        public Void invoke(File d, VirtualChannel channel) throws IOException {
228
            if (!Functions.isWindows()) {
×
229
                process(d);
×
230
            }
231
            return null;
×
232
        }
233

234
        private void process(File f) {
235
            if (f.isFile()) {
×
236
                f.setExecutable(true, false); // NOSONAR
×
237
            }
238
        }
×
239
    }
240

241
    public static class KubectlInstallable extends Installable implements NodeSpecific<KubectlInstallable> {
242
        public String cmd;
243

244
        public KubectlInstallable(String id, String name) {
×
245
            this.id = id;
×
246
            this.name = name;
×
247
        }
×
248

249
        @Override
250
        public KubectlInstallable forNode(Node node, TaskListener log) throws IOException, InterruptedException {
251
            String cmd;
252
            String platform;
253
            switch (Platform.of(node)) {
×
254
            case LINUX:
255
                platform = "linux";
×
256
                cmd = "kubectl";
×
257
                break;
×
258
            case MACOS:
259
                platform = "darwn";
×
260
                cmd = "kubectl";
×
261
                break;
×
262
            case WINDOWS:
263
                platform = "windows";
×
264
                cmd = "kubectl.exe";
×
265
                break;
×
266
            default:
267
                throw new IllegalStateException("Unmanaged node platform");
×
268
            }
269

270
            KubectlInstallable clone = new KubectlInstallable(id, name);
×
271
            clone.cmd = cmd;
×
272
            clone.url = "https://dl.k8s.io/release/" + id + "/bin/" + platform + "/amd64/" + cmd;
×
273
            return clone;
×
274
        }
275

276
    }
277
}
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