Skip to content

Commit d34a04e

Browse files
committed
feat(provider/kubernetes): support for kubectl server-side-apply strategy
kubernetes server-side apply (SSA) was released back in 1.14 and became GA In 1.22. This new strategy will use the new merging algorithm, as well as tracking field ownership at the kubernetes api-server Signed-off-by: Amir Alavi <amiralavi7@gmail.com>
1 parent 3cbd478 commit d34a04e

File tree

7 files changed

+144
-4
lines changed

7 files changed

+144
-4
lines changed

clouddriver-kubernetes/src/integration/java/com/netflix/spinnaker/clouddriver/kubernetes/it/DeployManifestIT.java

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1776,4 +1776,113 @@ public void shouldUseSourceCapacityVersioned() throws IOException, InterruptedEx
17761776
+ " -o=jsonpath='{.spec.template.spec.containers[0].env[0].value}'");
17771777
assertEquals("test", envVarValue, "Expected update env var for " + appName + " replicaset.\n");
17781778
}
1779+
1780+
@DisplayName(
1781+
".\n===\n"
1782+
+ "Given a deployment manifest with server-side-apply strategy set\n"
1783+
+ "When sending deploy manifest request\n"
1784+
+ "Then a deployment is created using server-side apply\n===")
1785+
@Test
1786+
public void shouldDeployUsingServerSideApply() throws IOException, InterruptedException {
1787+
// ------------------------- given --------------------------
1788+
String appName = "server-side-apply";
1789+
System.out.println("> Using namespace: " + account1Ns + ", appName: " + appName);
1790+
List<Map<String, Object>> manifest =
1791+
KubeTestUtils.loadYaml("classpath:manifests/deployment.yml")
1792+
.withValue("metadata.namespace", account1Ns)
1793+
.withValue("metadata.name", DEPLOYMENT_1_NAME)
1794+
.withValue(
1795+
"metadata.annotations",
1796+
ImmutableMap.of("strategy.spinnaker.io/server-side-apply", "true"))
1797+
.asList();
1798+
1799+
// ------------------------- when --------------------------
1800+
List<Map<String, Object>> body =
1801+
KubeTestUtils.loadJson("classpath:requests/deploy_manifest.json")
1802+
.withValue("deployManifest.account", ACCOUNT1_NAME)
1803+
.withValue("deployManifest.moniker.app", appName)
1804+
.withValue("deployManifest.manifests", manifest)
1805+
.asList();
1806+
KubeTestUtils.deployAndWaitStable(
1807+
baseUrl(), body, account1Ns, "deployment " + DEPLOYMENT_1_NAME);
1808+
1809+
// ------------------------- then --------------------------
1810+
/* Expecting:
1811+
metadata:
1812+
managedFields:
1813+
- manager: kubectl
1814+
operation: Apply
1815+
fieldsType: FieldsV1
1816+
*/
1817+
String managedFields =
1818+
kubeCluster.execKubectl(
1819+
"-n "
1820+
+ account1Ns
1821+
+ " get deployment "
1822+
+ DEPLOYMENT_1_NAME
1823+
+ " -o=jsonpath='{.metadata.managedFields}'");
1824+
assertTrue(
1825+
Strings.isNotEmpty(managedFields),
1826+
"Expected managedFields for "
1827+
+ DEPLOYMENT_1_NAME
1828+
+ " deployment to exist and be managed server-side. managedFields:\n"
1829+
+ managedFields);
1830+
1831+
String applyManager =
1832+
kubeCluster.execKubectl(
1833+
"-n "
1834+
+ account1Ns
1835+
+ " get deployment "
1836+
+ DEPLOYMENT_1_NAME
1837+
+ " -o=jsonpath='{.metadata.managedFields[?(@.operation==\"Apply\")].manager}'");
1838+
assertEquals(
1839+
"kubectl",
1840+
applyManager,
1841+
"Expected apply manager for "
1842+
+ DEPLOYMENT_1_NAME
1843+
+ " deployment to be managed server-side. managedFields:\n"
1844+
+ managedFields);
1845+
}
1846+
1847+
@DisplayName(
1848+
".\n===\n"
1849+
+ "Given a deployment manifest without a strategy set\n"
1850+
+ "When sending deploy manifest request\n"
1851+
+ "Then a deployment is created using client-side apply\n===")
1852+
@Test
1853+
public void shouldDeployUsingClientApply() throws IOException, InterruptedException {
1854+
// ------------------------- given --------------------------
1855+
String appName = "client-side-apply";
1856+
System.out.println("> Using namespace: " + account1Ns + ", appName: " + appName);
1857+
List<Map<String, Object>> manifest =
1858+
KubeTestUtils.loadYaml("classpath:manifests/deployment.yml")
1859+
.withValue("metadata.namespace", account1Ns)
1860+
.withValue("metadata.name", DEPLOYMENT_1_NAME)
1861+
.asList();
1862+
1863+
// ------------------------- when --------------------------
1864+
List<Map<String, Object>> body =
1865+
KubeTestUtils.loadJson("classpath:requests/deploy_manifest.json")
1866+
.withValue("deployManifest.account", ACCOUNT1_NAME)
1867+
.withValue("deployManifest.moniker.app", appName)
1868+
.withValue("deployManifest.manifests", manifest)
1869+
.asList();
1870+
KubeTestUtils.deployAndWaitStable(
1871+
baseUrl(), body, account1Ns, "deployment " + DEPLOYMENT_1_NAME);
1872+
1873+
// ------------------------- then --------------------------
1874+
String lastAppliedConfiguration =
1875+
kubeCluster.execKubectl(
1876+
"-n "
1877+
+ account1Ns
1878+
+ " get deployment "
1879+
+ DEPLOYMENT_1_NAME
1880+
+ " -o=jsonpath='{.metadata.annotations.kubectl\\.kubernetes\\.io/last-applied-configuration}'");
1881+
assertTrue(
1882+
Strings.isNotEmpty(lastAppliedConfiguration),
1883+
"Expected last-applied-configuration for "
1884+
+ DEPLOYMENT_1_NAME
1885+
+ " deployment to exist and be managed client-side. fields:\n"
1886+
+ lastAppliedConfiguration);
1887+
}
17791888
}

clouddriver-kubernetes/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/description/manifest/KubernetesManifestStrategy.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ ImmutableMap<String, String> toAnnotations() {
107107
public enum DeployStrategy {
108108
APPLY(null),
109109
RECREATE(STRATEGY_ANNOTATION_PREFIX + "/recreate"),
110-
REPLACE(STRATEGY_ANNOTATION_PREFIX + "/replace");
110+
REPLACE(STRATEGY_ANNOTATION_PREFIX + "/replace"),
111+
SERVER_SIDE_APPLY(STRATEGY_ANNOTATION_PREFIX + "/server-side-apply");
111112

112113
@Nullable private final String annotation;
113114

@@ -122,6 +123,9 @@ static DeployStrategy fromAnnotations(Map<String, String> annotations) {
122123
if (Boolean.parseBoolean(annotations.get(REPLACE.annotation))) {
123124
return REPLACE;
124125
}
126+
if (Boolean.parseBoolean(annotations.get(SERVER_SIDE_APPLY.annotation))) {
127+
return SERVER_SIDE_APPLY;
128+
}
125129
return APPLY;
126130
}
127131

clouddriver-kubernetes/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/op/handler/CanDeploy.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ default OperationResult deploy(
5959
case REPLACE:
6060
deployedManifest = credentials.createOrReplace(manifest, task, opName);
6161
break;
62+
case SERVER_SIDE_APPLY:
63+
deployedManifest = credentials.deploy(manifest, task, opName, "--server-side");
64+
break;
6265
case APPLY:
6366
deployedManifest = credentials.deploy(manifest, task, opName);
6467
break;

clouddriver-kubernetes/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/op/job/KubectlJobExecutor.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -587,12 +587,17 @@ public ImmutableList<KubernetesManifest> list(
587587
}
588588

589589
public KubernetesManifest deploy(
590-
KubernetesCredentials credentials, KubernetesManifest manifest, Task task, String opName) {
590+
KubernetesCredentials credentials,
591+
KubernetesManifest manifest,
592+
Task task,
593+
String opName,
594+
String... cmdArgs) {
591595
log.info("Deploying manifest {}", manifest.getFullResourceName());
592596
List<String> command = kubectlAuthPrefix(credentials);
593597

594598
// Read from stdin
595599
command.add("apply");
600+
command.addAll(List.of(cmdArgs));
596601
command.add("-o");
597602
command.add("json");
598603
command.add("-f");

clouddriver-kubernetes/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/security/KubernetesCredentials.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -561,12 +561,13 @@ public Collection<KubernetesPodMetric> topPod(KubernetesCoordinates coords) {
561561
() -> jobExecutor.topPod(this, coords.getNamespace(), coords.getName()));
562562
}
563563

564-
public KubernetesManifest deploy(KubernetesManifest manifest, Task task, String opName) {
564+
public KubernetesManifest deploy(
565+
KubernetesManifest manifest, Task task, String opName, String... cmdArgs) {
565566
return runAndRecordMetrics(
566567
"deploy",
567568
manifest.getKind(),
568569
manifest.getNamespace(),
569-
() -> jobExecutor.deploy(this, manifest, task, opName));
570+
() -> jobExecutor.deploy(this, manifest, task, opName, cmdArgs));
570571
}
571572

572573
private KubernetesManifest replace(KubernetesManifest manifest, Task task, String opName) {

clouddriver-kubernetes/src/test/java/com/netflix/spinnaker/clouddriver/kubernetes/description/manifest/KubernetesManifestStrategyTest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ void replaceStrategy() {
6464
assertThat(strategy).isEqualTo(DeployStrategy.REPLACE);
6565
}
6666

67+
@Test
68+
void serverSideApplyStrategy() {
69+
KubernetesManifestStrategy.DeployStrategy strategy =
70+
KubernetesManifestStrategy.DeployStrategy.fromAnnotations(
71+
ImmutableMap.of("strategy.spinnaker.io/server-side-apply", "true"));
72+
assertThat(strategy).isEqualTo(DeployStrategy.SERVER_SIDE_APPLY);
73+
}
74+
6775
@Test
6876
void nonBooleanValue() {
6977
KubernetesManifestStrategy.DeployStrategy strategy =

clouddriver-kubernetes/src/test/java/com/netflix/spinnaker/clouddriver/kubernetes/op/handler/CanDeployTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,16 @@ void applyReturnValue() {
6161
assertThat(result.getManifests()).containsExactlyInAnyOrder(manifest);
6262
}
6363

64+
@Test
65+
void applyServerSideMutations() {
66+
KubernetesCredentials credentials = mock(KubernetesCredentials.class);
67+
KubernetesManifest manifest = ManifestFetcher.getManifest("candeploy/deployment.yml");
68+
when(credentials.deploy(manifest, task, OP_NAME, "--server-side")).thenReturn(manifest);
69+
handler.deploy(credentials, manifest, DeployStrategy.SERVER_SIDE_APPLY, task, OP_NAME);
70+
verify(credentials).deploy(manifest, task, OP_NAME, "--server-side");
71+
verifyNoMoreInteractions(credentials);
72+
}
73+
6474
@Test
6575
void replaceMutations() {
6676
KubernetesCredentials credentials = mock(KubernetesCredentials.class);

0 commit comments

Comments
 (0)