Skip to content

Commit ce121b3

Browse files
a7imergify[bot]
andauthored
feat(provider/kubernetes): support for kubectl server-side-apply strategy (#5989)
* 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> * server-side-apply: support force-conflicts Signed-off-by: Amir Alavi <amiralavi7@gmail.com> --------- Signed-off-by: Amir Alavi <amiralavi7@gmail.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 44124e2 commit ce121b3

File tree

8 files changed

+390
-14
lines changed

8 files changed

+390
-14
lines changed

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

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1785,4 +1785,159 @@ public void shouldUseSourceCapacityVersioned() throws IOException, InterruptedEx
17851785
+ " -o=jsonpath='{.spec.template.spec.containers[0].env[0].value}'");
17861786
assertEquals("test", envVarValue, "Expected update env var for " + appName + " replicaset.\n");
17871787
}
1788+
1789+
@DisplayName(
1790+
".\n===\n"
1791+
+ "Given a deployment manifest with server-side-apply strategy set\n"
1792+
+ "When sending deploy manifest request\n"
1793+
+ "Then a deployment is created using server-side apply\n===")
1794+
@Test
1795+
public void shouldDeployUsingServerSideApply() throws IOException, InterruptedException {
1796+
// ------------------------- given --------------------------
1797+
String appName = "server-side-apply";
1798+
System.out.println("> Using namespace: " + account1Ns + ", appName: " + appName);
1799+
List<Map<String, Object>> manifest =
1800+
KubeTestUtils.loadYaml("classpath:manifests/deployment.yml")
1801+
.withValue("metadata.namespace", account1Ns)
1802+
.withValue("metadata.name", DEPLOYMENT_1_NAME)
1803+
.withValue(
1804+
"metadata.annotations",
1805+
ImmutableMap.of("strategy.spinnaker.io/server-side-apply", "force-conflicts"))
1806+
.asList();
1807+
1808+
// ------------------------- when --------------------------
1809+
List<Map<String, Object>> body =
1810+
KubeTestUtils.loadJson("classpath:requests/deploy_manifest.json")
1811+
.withValue("deployManifest.account", ACCOUNT1_NAME)
1812+
.withValue("deployManifest.moniker.app", appName)
1813+
.withValue("deployManifest.manifests", manifest)
1814+
.asList();
1815+
KubeTestUtils.deployAndWaitStable(
1816+
baseUrl(), body, account1Ns, "deployment " + DEPLOYMENT_1_NAME);
1817+
1818+
// ------------------------- then --------------------------
1819+
/* Expecting:
1820+
metadata:
1821+
managedFields:
1822+
- manager: kubectl
1823+
operation: Apply
1824+
fieldsType: FieldsV1
1825+
*/
1826+
String managedFields =
1827+
kubeCluster.execKubectl(
1828+
"-n "
1829+
+ account1Ns
1830+
+ " get deployment "
1831+
+ DEPLOYMENT_1_NAME
1832+
+ " -o=jsonpath='{.metadata.managedFields}'");
1833+
assertTrue(
1834+
Strings.isNotEmpty(managedFields),
1835+
"Expected managedFields for "
1836+
+ DEPLOYMENT_1_NAME
1837+
+ " deployment to exist and be managed server-side. managedFields:\n"
1838+
+ managedFields);
1839+
1840+
String applyManager =
1841+
kubeCluster.execKubectl(
1842+
"-n "
1843+
+ account1Ns
1844+
+ " get deployment "
1845+
+ DEPLOYMENT_1_NAME
1846+
+ " -o=jsonpath='{.metadata.managedFields[?(@.operation==\"Apply\")].manager}'");
1847+
assertEquals(
1848+
"kubectl",
1849+
applyManager,
1850+
"Expected apply manager for "
1851+
+ DEPLOYMENT_1_NAME
1852+
+ " deployment to be managed server-side. managedFields:\n"
1853+
+ managedFields);
1854+
}
1855+
1856+
@DisplayName(
1857+
".\n===\n"
1858+
+ "Given a deployment manifest with server-side-apply disabled set\n"
1859+
+ "When sending deploy manifest request\n"
1860+
+ "Then a deployment is created using client-side apply\n===")
1861+
@Test
1862+
public void shouldDeployUsingApplyWithServerSideApplyDisabled()
1863+
throws IOException, InterruptedException {
1864+
// ------------------------- given --------------------------
1865+
String appName = "server-side-apply-disabled";
1866+
System.out.println("> Using namespace: " + account1Ns + ", appName: " + appName);
1867+
List<Map<String, Object>> manifest =
1868+
KubeTestUtils.loadYaml("classpath:manifests/deployment.yml")
1869+
.withValue("metadata.namespace", account1Ns)
1870+
.withValue("metadata.name", DEPLOYMENT_1_NAME)
1871+
.withValue(
1872+
"metadata.annotations",
1873+
ImmutableMap.of("strategy.spinnaker.io/server-side-apply", "false"))
1874+
.asList();
1875+
1876+
// ------------------------- when --------------------------
1877+
List<Map<String, Object>> body =
1878+
KubeTestUtils.loadJson("classpath:requests/deploy_manifest.json")
1879+
.withValue("deployManifest.account", ACCOUNT1_NAME)
1880+
.withValue("deployManifest.moniker.app", appName)
1881+
.withValue("deployManifest.manifests", manifest)
1882+
.asList();
1883+
KubeTestUtils.deployAndWaitStable(
1884+
baseUrl(), body, account1Ns, "deployment " + DEPLOYMENT_1_NAME);
1885+
1886+
// ------------------------- then --------------------------
1887+
String lastAppliedConfiguration =
1888+
kubeCluster.execKubectl(
1889+
"-n "
1890+
+ account1Ns
1891+
+ " get deployment "
1892+
+ DEPLOYMENT_1_NAME
1893+
+ " -o=jsonpath='{.metadata.annotations.kubectl\\.kubernetes\\.io/last-applied-configuration}'");
1894+
assertTrue(
1895+
Strings.isNotEmpty(lastAppliedConfiguration),
1896+
"Expected last-applied-configuration for "
1897+
+ DEPLOYMENT_1_NAME
1898+
+ " deployment to exist and be managed client-side. fields:\n"
1899+
+ lastAppliedConfiguration);
1900+
}
1901+
1902+
@DisplayName(
1903+
".\n===\n"
1904+
+ "Given a deployment manifest without a strategy set\n"
1905+
+ "When sending deploy manifest request\n"
1906+
+ "Then a deployment is created using client-side apply\n===")
1907+
@Test
1908+
public void shouldDeployUsingClientApply() throws IOException, InterruptedException {
1909+
// ------------------------- given --------------------------
1910+
String appName = "client-side-apply";
1911+
System.out.println("> Using namespace: " + account1Ns + ", appName: " + appName);
1912+
List<Map<String, Object>> manifest =
1913+
KubeTestUtils.loadYaml("classpath:manifests/deployment.yml")
1914+
.withValue("metadata.namespace", account1Ns)
1915+
.withValue("metadata.name", DEPLOYMENT_1_NAME)
1916+
.asList();
1917+
1918+
// ------------------------- when --------------------------
1919+
List<Map<String, Object>> body =
1920+
KubeTestUtils.loadJson("classpath:requests/deploy_manifest.json")
1921+
.withValue("deployManifest.account", ACCOUNT1_NAME)
1922+
.withValue("deployManifest.moniker.app", appName)
1923+
.withValue("deployManifest.manifests", manifest)
1924+
.asList();
1925+
KubeTestUtils.deployAndWaitStable(
1926+
baseUrl(), body, account1Ns, "deployment " + DEPLOYMENT_1_NAME);
1927+
1928+
// ------------------------- then --------------------------
1929+
String lastAppliedConfiguration =
1930+
kubeCluster.execKubectl(
1931+
"-n "
1932+
+ account1Ns
1933+
+ " get deployment "
1934+
+ DEPLOYMENT_1_NAME
1935+
+ " -o=jsonpath='{.metadata.annotations.kubectl\\.kubernetes\\.io/last-applied-configuration}'");
1936+
assertTrue(
1937+
Strings.isNotEmpty(lastAppliedConfiguration),
1938+
"Expected last-applied-configuration for "
1939+
+ DEPLOYMENT_1_NAME
1940+
+ " deployment to exist and be managed client-side. fields:\n"
1941+
+ lastAppliedConfiguration);
1942+
}
17881943
}

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

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,29 +40,38 @@ public final class KubernetesManifestStrategy {
4040
private static final String USE_SOURCE_CAPACITY =
4141
STRATEGY_ANNOTATION_PREFIX + "/use-source-capacity";
4242

43+
private static final String SERVER_SIDE_APPLY_STRATEGY =
44+
STRATEGY_ANNOTATION_PREFIX + "/server-side-apply";
45+
private static final String SERVER_SIDE_APPLY_FORCE_CONFLICTS = "force-conflicts";
46+
4347
private final DeployStrategy deployStrategy;
4448
private final Versioned versioned;
4549
private final OptionalInt maxVersionHistory;
4650
private final boolean useSourceCapacity;
51+
private final ServerSideApplyStrategy serverSideApplyStrategy;
4752

4853
@Builder
4954
@ParametersAreNullableByDefault
5055
private KubernetesManifestStrategy(
5156
DeployStrategy deployStrategy,
5257
Versioned versioned,
5358
Integer maxVersionHistory,
54-
boolean useSourceCapacity) {
59+
boolean useSourceCapacity,
60+
ServerSideApplyStrategy serverSideApplyStrategy) {
5561
this.deployStrategy = Optional.ofNullable(deployStrategy).orElse(DeployStrategy.APPLY);
5662
this.versioned = Optional.ofNullable(versioned).orElse(Versioned.DEFAULT);
5763
this.maxVersionHistory =
5864
maxVersionHistory == null ? OptionalInt.empty() : OptionalInt.of(maxVersionHistory);
5965
this.useSourceCapacity = useSourceCapacity;
66+
this.serverSideApplyStrategy =
67+
Optional.ofNullable(serverSideApplyStrategy).orElse(ServerSideApplyStrategy.DEFAULT);
6068
}
6169

6270
static KubernetesManifestStrategy fromAnnotations(Map<String, String> annotations) {
6371
return KubernetesManifestStrategy.builder()
6472
.versioned(Versioned.fromAnnotations(annotations))
6573
.deployStrategy(DeployStrategy.fromAnnotations(annotations))
74+
.serverSideApplyStrategy(ServerSideApplyStrategy.fromAnnotations(annotations))
6675
.useSourceCapacity(Boolean.parseBoolean(annotations.get(USE_SOURCE_CAPACITY)))
6776
.maxVersionHistory(Ints.tryParse(annotations.getOrDefault(MAX_VERSION_HISTORY, "")))
6877
.build();
@@ -72,6 +81,7 @@ ImmutableMap<String, String> toAnnotations() {
7281
ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
7382
builder.putAll(deployStrategy.toAnnotations());
7483
builder.putAll(versioned.toAnnotations());
84+
builder.putAll(serverSideApplyStrategy.toAnnotations());
7585
if (maxVersionHistory.isPresent()) {
7686
builder.put(MAX_VERSION_HISTORY, Integer.toString(maxVersionHistory.getAsInt()));
7787
}
@@ -107,7 +117,8 @@ ImmutableMap<String, String> toAnnotations() {
107117
public enum DeployStrategy {
108118
APPLY(null),
109119
RECREATE(STRATEGY_ANNOTATION_PREFIX + "/recreate"),
110-
REPLACE(STRATEGY_ANNOTATION_PREFIX + "/replace");
120+
REPLACE(STRATEGY_ANNOTATION_PREFIX + "/replace"),
121+
SERVER_SIDE_APPLY(SERVER_SIDE_APPLY_STRATEGY);
111122

112123
@Nullable private final String annotation;
113124

@@ -122,6 +133,11 @@ static DeployStrategy fromAnnotations(Map<String, String> annotations) {
122133
if (Boolean.parseBoolean(annotations.get(REPLACE.annotation))) {
123134
return REPLACE;
124135
}
136+
if (annotations.containsKey(SERVER_SIDE_APPLY.annotation)
137+
&& ServerSideApplyStrategy.fromAnnotations(annotations)
138+
!= ServerSideApplyStrategy.DISABLED) {
139+
return SERVER_SIDE_APPLY;
140+
}
125141
return APPLY;
126142
}
127143

@@ -142,4 +158,33 @@ void setAnnotations(Map<String, String> annotations) {
142158
annotations.putAll(toAnnotations());
143159
}
144160
}
161+
162+
public enum ServerSideApplyStrategy {
163+
FORCE_CONFLICTS(ImmutableMap.of(SERVER_SIDE_APPLY_STRATEGY, SERVER_SIDE_APPLY_FORCE_CONFLICTS)),
164+
DISABLED(ImmutableMap.of(SERVER_SIDE_APPLY_STRATEGY, Boolean.FALSE.toString())),
165+
DEFAULT(ImmutableMap.of());
166+
private final ImmutableMap<String, String> annotations;
167+
168+
ServerSideApplyStrategy(ImmutableMap<String, String> annotations) {
169+
this.annotations = annotations;
170+
}
171+
172+
static ServerSideApplyStrategy fromAnnotations(Map<String, String> annotations) {
173+
if (annotations.containsKey(SERVER_SIDE_APPLY_STRATEGY)) {
174+
String strategy = annotations.get(SERVER_SIDE_APPLY_STRATEGY);
175+
if (Boolean.parseBoolean(strategy)) {
176+
return DEFAULT;
177+
}
178+
179+
if (strategy.equals(SERVER_SIDE_APPLY_FORCE_CONFLICTS)) {
180+
return FORCE_CONFLICTS;
181+
}
182+
}
183+
return DISABLED;
184+
}
185+
186+
ImmutableMap<String, String> toAnnotations() {
187+
return annotations;
188+
}
189+
}
145190
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,15 @@
2525
import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesCredentials;
2626
import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesSelectorList;
2727
import io.kubernetes.client.openapi.models.V1DeleteOptions;
28+
import java.util.ArrayList;
29+
import java.util.List;
2830

2931
public interface CanDeploy {
3032
default OperationResult deploy(
3133
KubernetesCredentials credentials,
3234
KubernetesManifest manifest,
3335
KubernetesManifestStrategy.DeployStrategy deployStrategy,
36+
KubernetesManifestStrategy.ServerSideApplyStrategy serverSideApplyStrategy,
3437
Task task,
3538
String opName) {
3639
// If the manifest has a generateName, we must apply with kubectl create as all other operations
@@ -59,6 +62,16 @@ default OperationResult deploy(
5962
case REPLACE:
6063
deployedManifest = credentials.createOrReplace(manifest, task, opName);
6164
break;
65+
case SERVER_SIDE_APPLY:
66+
List<String> cmdArgs = new ArrayList<>();
67+
cmdArgs.add("--server-side=true");
68+
if (serverSideApplyStrategy.equals(
69+
KubernetesManifestStrategy.ServerSideApplyStrategy.FORCE_CONFLICTS)) {
70+
cmdArgs.add("--force-conflicts=true");
71+
}
72+
deployedManifest =
73+
credentials.deploy(manifest, task, opName, cmdArgs.toArray(new String[cmdArgs.size()]));
74+
break;
6275
case APPLY:
6376
deployedManifest = credentials.deploy(manifest, task, opName);
6477
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/op/manifest/KubernetesDeployManifestOperation.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,12 @@ public OperationResult operate(List<OperationResult> _unused) {
158158
+ " to kubernetes master...");
159159
result.merge(
160160
deployer.deploy(
161-
credentials, holder.manifest, strategy.getDeployStrategy(), getTask(), OP_NAME));
161+
credentials,
162+
holder.manifest,
163+
strategy.getDeployStrategy(),
164+
strategy.getServerSideApplyStrategy(),
165+
getTask(),
166+
OP_NAME));
162167

163168
result.getCreatedArtifacts().add(holder.artifact);
164169
getTask()

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) {

0 commit comments

Comments
 (0)