|
16 | 16 | import com.microsoft.jenkins.kubernetes.KubernetesCDPlugin; |
17 | 17 | import com.microsoft.jenkins.kubernetes.KubernetesClientWrapper; |
18 | 18 | import com.microsoft.jenkins.kubernetes.Messages; |
| 19 | +import com.microsoft.jenkins.kubernetes.credentials.ClientWrapperFactory; |
| 20 | +import com.microsoft.jenkins.kubernetes.credentials.ResolvedDockerRegistryEndpoint; |
19 | 21 | import com.microsoft.jenkins.kubernetes.util.Constants; |
20 | 22 | import hudson.EnvVars; |
21 | 23 | import hudson.FilePath; |
22 | 24 | import hudson.model.Item; |
| 25 | +import hudson.model.TaskListener; |
23 | 26 | import hudson.util.VariableResolver; |
| 27 | +import io.fabric8.kubernetes.client.KubernetesClient; |
| 28 | +import jenkins.security.MasterToSlaveCallable; |
24 | 29 | import org.apache.commons.lang3.StringUtils; |
25 | | -import org.jenkinsci.plugins.docker.commons.credentials.DockerRegistryEndpoint; |
26 | 30 |
|
| 31 | +import java.io.IOException; |
| 32 | +import java.io.Serializable; |
27 | 33 | import java.net.URL; |
| 34 | +import java.util.HashMap; |
28 | 35 | import java.util.List; |
| 36 | +import java.util.Map; |
29 | 37 |
|
30 | 38 | import static com.google.common.base.Preconditions.checkState; |
31 | 39 |
|
32 | | -public class DeploymentCommand implements ICommand<DeploymentCommand.IDeploymentCommand> { |
| 40 | +/** |
| 41 | + * Command to deploy Kubernetes configurations. |
| 42 | + * <p> |
| 43 | + * Mark it as serializable so that the inner Callable can be serialized correctly. |
| 44 | + */ |
| 45 | +public class DeploymentCommand implements ICommand<DeploymentCommand.IDeploymentCommand>, Serializable { |
33 | 46 | @Override |
34 | 47 | public void execute(IDeploymentCommand context) { |
35 | 48 | JobContext jobContext = context.getJobContext(); |
36 | | - FilePath workspace = jobContext.getWorkspace(); |
37 | | - Item jobItem = jobContext.getRun().getParent(); |
38 | | - EnvVars envVars = context.getEnvVars(); |
39 | | - String secretNamespace = context.getSecretNamespace(); |
40 | | - String configPaths = context.getConfigs(); |
41 | 49 |
|
42 | | - KubernetesClientWrapper wrapper = null; |
| 50 | + // all the final variables below are serializable, which will be captured in the below MasterToSlaveCallable |
| 51 | + // and execute on the slave if the job is scheduled on slave. |
| 52 | + final TaskListener taskListener = jobContext.getTaskListener(); |
| 53 | + final String secretNameCfg = context.getSecretName(); |
| 54 | + final String secretNamespace = context.getSecretNamespace(); |
| 55 | + final String configPaths = context.getConfigs(); |
| 56 | + final FilePath workspace = jobContext.getWorkspace(); |
| 57 | + final String defaultSecretName = jobContext.getRun().getDisplayName(); |
| 58 | + final EnvVars envVars = context.getEnvVars(); |
| 59 | + final boolean enableSubstitution = context.isEnableConfigSubstitution(); |
| 60 | + final ClientWrapperFactory clientFactory = context.clientFactory(); |
| 61 | + |
| 62 | + TaskResult taskResult = null; |
43 | 63 | try { |
44 | | - checkState(StringUtils.isNotBlank(secretNamespace), Messages.DeploymentCommand_blankNamespace()); |
45 | | - checkState(StringUtils.isNotBlank(configPaths), Messages.DeploymentCommand_blankConfigFiles()); |
46 | | - |
47 | | - wrapper = context.buildKubernetesClientWrapper(workspace).withLogger(jobContext.logger()); |
48 | | - |
49 | | - FilePath[] configFiles = workspace.list(configPaths); |
50 | | - if (configFiles.length == 0) { |
51 | | - context.logError(Messages.DeploymentCommand_noMatchingConfigFiles(configPaths)); |
52 | | - return; |
53 | | - } |
54 | | - |
55 | | - List<DockerRegistryEndpoint> dockerCredentials = context.getDockerCredentials(); |
56 | | - if (!dockerCredentials.isEmpty()) { |
57 | | - String secretName = KubernetesClientWrapper.prepareSecretName( |
58 | | - context.getSecretName(), jobContext.getRun().getDisplayName(), envVars); |
59 | | - |
60 | | - wrapper.createOrReplaceSecrets(jobItem, secretNamespace, secretName, dockerCredentials); |
61 | | - |
62 | | - context.logStatus(Messages.DeploymentCommand_injectSecretName( |
63 | | - Constants.KUBERNETES_SECRET_NAME_PROP, secretName)); |
64 | | - EnvironmentInjector.inject( |
65 | | - jobContext.getRun(), envVars, Constants.KUBERNETES_SECRET_NAME_PROP, secretName); |
| 64 | + final List<ResolvedDockerRegistryEndpoint> dockerRegistryEndpoints = |
| 65 | + context.resolveEndpoints(jobContext.getRun().getParent()); |
| 66 | + taskResult = workspace.act(new MasterToSlaveCallable<TaskResult, Exception>() { |
| 67 | + @Override |
| 68 | + public TaskResult call() throws Exception { |
| 69 | + TaskResult result = new TaskResult(); |
| 70 | + |
| 71 | + checkState(StringUtils.isNotBlank(secretNamespace), Messages.DeploymentCommand_blankNamespace()); |
| 72 | + checkState(StringUtils.isNotBlank(configPaths), Messages.DeploymentCommand_blankConfigFiles()); |
| 73 | + |
| 74 | + KubernetesClientWrapper wrapper = |
| 75 | + clientFactory.buildClient(workspace).withLogger(taskListener.getLogger()); |
| 76 | + result.masterHost = getMasterHost(wrapper); |
| 77 | + |
| 78 | + FilePath[] configFiles = workspace.list(configPaths); |
| 79 | + if (configFiles.length == 0) { |
| 80 | + String message = Messages.DeploymentCommand_noMatchingConfigFiles(configPaths); |
| 81 | + taskListener.error(message); |
| 82 | + result.commandState = CommandState.HasError; |
| 83 | + throw new IllegalStateException(message); |
| 84 | + } |
| 85 | + |
| 86 | + if (!dockerRegistryEndpoints.isEmpty()) { |
| 87 | + String secretName = |
| 88 | + KubernetesClientWrapper.prepareSecretName(secretNameCfg, defaultSecretName, envVars); |
| 89 | + |
| 90 | + wrapper.createOrReplaceSecrets(secretNamespace, secretName, dockerRegistryEndpoints); |
| 91 | + |
| 92 | + taskListener.getLogger().println(Messages.DeploymentCommand_injectSecretName( |
| 93 | + Constants.KUBERNETES_SECRET_NAME_PROP, secretName)); |
| 94 | + envVars.put(Constants.KUBERNETES_SECRET_NAME_PROP, secretName); |
| 95 | + result.extraEnvVars.put(Constants.KUBERNETES_SECRET_NAME_PROP, secretName); |
| 96 | + } |
| 97 | + |
| 98 | + if (enableSubstitution) { |
| 99 | + wrapper.withVariableResolver(new VariableResolver.ByMap<>(envVars)); |
| 100 | + } |
| 101 | + |
| 102 | + wrapper.apply(configFiles); |
| 103 | + |
| 104 | + result.commandState = CommandState.Success; |
| 105 | + |
| 106 | + return result; |
| 107 | + } |
| 108 | + }); |
| 109 | + for (Map.Entry<String, String> entry : taskResult.extraEnvVars.entrySet()) { |
| 110 | + EnvironmentInjector.inject(jobContext.getRun(), envVars, entry.getKey(), entry.getValue()); |
66 | 111 | } |
67 | 112 |
|
68 | | - if (context.isEnableConfigSubstitution()) { |
69 | | - wrapper.withVariableResolver(new VariableResolver.ByMap<>(envVars)); |
| 113 | + context.setCommandState(taskResult.commandState); |
| 114 | + if (taskResult.commandState.isError()) { |
| 115 | + KubernetesCDPlugin.sendEvent(Constants.AI_KUBERNETES, "DeployFailed", |
| 116 | + Constants.AI_K8S_MASTER, AppInsightsUtils.hash(taskResult.masterHost)); |
| 117 | + } else { |
| 118 | + KubernetesCDPlugin.sendEvent(Constants.AI_KUBERNETES, "Deployed", |
| 119 | + Constants.AI_K8S_MASTER, AppInsightsUtils.hash(taskResult.masterHost)); |
70 | 120 | } |
71 | | - |
72 | | - wrapper.apply(configFiles); |
73 | | - |
74 | | - context.setCommandState(CommandState.Success); |
75 | | - |
76 | | - KubernetesCDPlugin.sendEvent(Constants.AI_KUBERNETES, "Deployed", |
77 | | - Constants.AI_K8S_MASTER, AppInsightsUtils.hash(getMasterHost(wrapper))); |
78 | 121 | } catch (Exception e) { |
79 | 122 | if (e instanceof InterruptedException) { |
80 | 123 | Thread.currentThread().interrupt(); |
81 | 124 | } |
82 | 125 | context.logError(e); |
83 | 126 | KubernetesCDPlugin.sendEvent(Constants.AI_KUBERNETES, "DeployFailed", |
84 | | - Constants.AI_K8S_MASTER, AppInsightsUtils.hash(getMasterHost(wrapper)), |
| 127 | + Constants.AI_K8S_MASTER, AppInsightsUtils.hash(taskResult == null ? null : taskResult.masterHost), |
85 | 128 | Constants.AI_MESSAGE, e.getMessage()); |
86 | 129 | } |
87 | 130 | } |
88 | 131 |
|
89 | 132 | @VisibleForTesting |
90 | 133 | String getMasterHost(KubernetesClientWrapper wrapper) { |
91 | 134 | if (wrapper != null) { |
92 | | - URL masterURL = wrapper.getClient().getMasterUrl(); |
93 | | - if (masterURL != null) { |
94 | | - return masterURL.getHost(); |
| 135 | + KubernetesClient client = wrapper.getClient(); |
| 136 | + if (client != null) { |
| 137 | + URL masterURL = client.getMasterUrl(); |
| 138 | + if (masterURL != null) { |
| 139 | + return masterURL.getHost(); |
| 140 | + } |
95 | 141 | } |
96 | 142 | } |
97 | 143 | return "Unknown"; |
98 | 144 | } |
99 | 145 |
|
| 146 | + public static class TaskResult implements Serializable { |
| 147 | + private static final long serialVersionUID = 1L; |
| 148 | + |
| 149 | + private CommandState commandState = CommandState.Unknown; |
| 150 | + private String masterHost; |
| 151 | + private final Map<String, String> extraEnvVars = new HashMap<>(); |
| 152 | + } |
| 153 | + |
100 | 154 | public interface IDeploymentCommand extends IBaseCommandData { |
101 | | - KubernetesClientWrapper buildKubernetesClientWrapper(FilePath workspace) throws Exception; |
| 155 | + ClientWrapperFactory clientFactory(); |
102 | 156 |
|
103 | 157 | String getSecretNamespace(); |
104 | 158 |
|
105 | 159 | String getSecretName(); |
106 | 160 |
|
107 | | - List<DockerRegistryEndpoint> getDockerCredentials(); |
| 161 | + List<ResolvedDockerRegistryEndpoint> resolveEndpoints(Item context) throws IOException; |
108 | 162 |
|
109 | 163 | String getConfigs(); |
110 | 164 |
|
|
0 commit comments