Skip to content

Commit b53ea06

Browse files
Sladyntimja
Sladyn
authored andcommitted
Schema Generation for nested yml configurations (#1027)
1 parent b53eb77 commit b53ea06

22 files changed

+484
-116
lines changed

integrations/pom.xml

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,12 @@
392392
<version>3.0.0</version>
393393
</dependency>
394394

395+
<dependency>
396+
<groupId>org.jenkins-ci.plugins</groupId>
397+
<artifactId>azure-keyvault</artifactId>
398+
<version>1.4</version>
399+
</dependency>
400+
395401
<dependency>
396402
<groupId>org.jenkins-ci.plugins</groupId>
397403
<artifactId>display-url-api</artifactId>
@@ -429,6 +435,31 @@
429435

430436
<dependencyManagement>
431437
<dependencies>
438+
<dependency>
439+
<groupId>com.microsoft.azure</groupId>
440+
<artifactId>azure-annotations</artifactId>
441+
<version>1.8.0</version>
442+
</dependency>
443+
<dependency>
444+
<groupId>io.reactivex</groupId>
445+
<artifactId>rxjava</artifactId>
446+
<version>1.3.8</version>
447+
</dependency>
448+
<dependency>
449+
<groupId>com.google.code.gson</groupId>
450+
<artifactId>gson</artifactId>
451+
<version>2.8.0</version>
452+
</dependency>
453+
<dependency>
454+
<groupId>com.squareup.okhttp3</groupId>
455+
<artifactId>logging-interceptor</artifactId>
456+
<version>3.12.2</version>
457+
</dependency>
458+
<dependency>
459+
<groupId>com.squareup.okhttp3</groupId>
460+
<artifactId>okhttp</artifactId>
461+
<version>3.12.2</version>
462+
</dependency>
432463

433464
<dependency>
434465
<groupId>com.github.docker-java</groupId>
@@ -525,7 +556,7 @@
525556
<dependency>
526557
<groupId>com.squareup.okio</groupId>
527558
<artifactId>okio</artifactId>
528-
<version>1.13.0</version>
559+
<version>1.15.0</version>
529560
</dependency>
530561
<dependency>
531562
<groupId>jaxen</groupId>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.jenkins.plugins.casc;
2+
3+
import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithReadmeRule;
4+
import org.junit.Rule;
5+
import org.junit.Test;
6+
7+
import static io.jenkins.plugins.casc.misc.Util.convertYamlFileToJson;
8+
import static io.jenkins.plugins.casc.misc.Util.validateSchema;
9+
import static org.hamcrest.Matchers.empty;
10+
import static org.junit.Assert.assertThat;
11+
12+
public class AzureKeyVaultTest {
13+
14+
@Rule
15+
public JenkinsConfiguredWithReadmeRule j = new JenkinsConfiguredWithReadmeRule();
16+
17+
@Test
18+
public void validJsonSchema() throws Exception {
19+
assertThat(
20+
validateSchema(convertYamlFileToJson(this, "azureKeyVault.yml")),
21+
empty());
22+
}
23+
}

integrations/src/test/java/io/jenkins/plugins/casc/SlackTest.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
import org.junit.contrib.java.lang.system.EnvironmentVariables;
1010
import org.junit.rules.RuleChain;
1111

12-
import static junit.framework.TestCase.assertNotNull;
12+
import static io.jenkins.plugins.casc.misc.Util.convertYamlFileToJson;
13+
import static io.jenkins.plugins.casc.misc.Util.validateSchema;
14+
import static org.hamcrest.Matchers.empty;
15+
import static org.junit.Assert.assertNotNull;
16+
import static org.junit.Assert.assertThat;
1317

1418
/**
1519
* @author v1v (Victor Martinez)
@@ -29,4 +33,11 @@ public void configure_slack() throws Exception {
2933
SlackNotifier.DescriptorImpl slackNotifier = ExtensionList.lookupSingleton(SlackNotifier.DescriptorImpl.class);
3034
assertNotNull(slackNotifier);
3135
}
36+
37+
@Test
38+
public void validJsonSchema() throws Exception {
39+
assertThat(
40+
validateSchema(convertYamlFileToJson(this, "slackSchema.yml")),
41+
empty());
42+
}
3243
}

integrations/src/test/java/io/jenkins/plugins/casc/SonarQubeTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@
66
import io.jenkins.plugins.casc.misc.ConfiguredWithReadme;
77
import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithReadmeRule;
88
import jenkins.model.GlobalConfiguration;
9+
import org.junit.Ignore;
910
import org.junit.Rule;
1011
import org.junit.Test;
1112

13+
import static io.jenkins.plugins.casc.misc.Util.convertYamlFileToJson;
14+
import static io.jenkins.plugins.casc.misc.Util.validateSchema;
15+
import static org.hamcrest.Matchers.empty;
1216
import static org.junit.Assert.assertEquals;
17+
import static org.junit.Assert.assertThat;
1318
import static org.junit.Assert.assertTrue;
1419

1520
/**
@@ -38,4 +43,18 @@ public void configure_sonar_globalconfig() throws Exception {
3843
assertEquals("envVar", triggers.getEnvVar());
3944
}
4045

46+
@Test
47+
public void validJsonSchema() throws Exception {
48+
assertThat(
49+
validateSchema(convertYamlFileToJson(this, "sonarSchema.yml")),
50+
empty());
51+
}
52+
53+
@Test
54+
@Ignore
55+
public void validFullJsonSchema() throws Exception {
56+
assertThat(
57+
validateSchema(convertYamlFileToJson(this, "sonarSchemaFull.yml")),
58+
empty());
59+
}
4160
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
unclassified:
2+
azureKeyVault:
3+
keyVaultURL: https://not-a-real-vault.vault.azure.net
4+
credentialID: service-principal
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
unclassified:
2+
slackNotifier:
3+
teamDomain: workspace
4+
tokenCredentialId: slack-token
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
unclassified:
2+
sonarGlobalConfiguration:
3+
buildWrapperEnabled: false
4+
installations:
5+
- additionalAnalysisProperties: sonar.organization=jenkinsci
6+
name: SonarQube
7+
serverAuthenticationToken: 'sonarcloud-api-token'
8+
serverUrl: 'https://sonarcloud.io'
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
unclassified:
2+
sonarGlobalConfiguration: # mandatory
3+
buildWrapperEnabled: true
4+
installations: # mandatory
5+
- name: "TEST" # id of the SonarQube configuration - to be used in jobs
6+
serverUrl: "http://url:9000"
7+
#credentialsId: token-sonarqube # id of the credentials containing sonar auth token (since 2.9 version)
8+
serverAuthenticationToken: "token" # for retrocompatibility with versions < 2.9
9+
mojoVersion: "mojoVersion"
10+
additionalProperties: "blah=blah"
11+
additionalAnalysisProperties: "additionalAnalysisProperties"
12+
triggers:
13+
skipScmCause: true
14+
skipUpstreamCause: true
15+
envVar: "envVar"

plugin/src/main/java/io/jenkins/plugins/casc/Attribute.java

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public class Attribute<Owner, Type> {
6161
private Setter<Owner, Type> setter;
6262
private Getter<Owner, Type> getter;
6363
private boolean secret;
64+
private boolean isJsonSchema;
6465

6566
private boolean deprecated;
6667

@@ -111,6 +112,14 @@ public Class<? extends AccessRestriction>[] getRestrictions() {
111112
return restrictions != null ? restrictions : EMPTY;
112113
}
113114

115+
/**
116+
* Set jsonSchema is used to tell the describe function to call the describe structure
117+
* so that it supports and returns a nested structure
118+
*/
119+
public void setJsonSchema(boolean jsonSchema) {
120+
isJsonSchema = jsonSchema;
121+
}
122+
114123
public boolean isRestricted() {
115124
return restrictions != null && restrictions.length > 0;
116125
}
@@ -235,8 +244,12 @@ public CNode describe(Owner instance, ConfigurationContext context) throws Confi
235244
if (multiple) {
236245
Sequence seq = new Sequence();
237246
if (o.getClass().isArray()) o = Arrays.asList((Object[]) o);
238-
for (Object value : (Iterable) o) {
239-
seq.add(_describe(c, context, value, shouldBeMasked));
247+
if (o instanceof Iterable) {
248+
for (Object value : (Iterable) o) {
249+
seq.add(_describe(c, context, value, shouldBeMasked));
250+
}
251+
} else {
252+
LOGGER.log(Level.FINE, o.getClass().toString() + " is not iterable");
240253
}
241254
return seq;
242255
}
@@ -249,6 +262,45 @@ public CNode describe(Owner instance, ConfigurationContext context) throws Confi
249262
}
250263
}
251264

265+
/**
266+
* This function is for the JSONSchemaGeneration
267+
* @param instance Owner Instance
268+
* @param context Context to be passed
269+
* @return CNode object describing the structure of the node
270+
*/
271+
public CNode describeForSchema (Owner instance, ConfigurationContext context) {
272+
final Configurator c = context.lookup(type);
273+
if (c == null) {
274+
return new Scalar("FAILED TO EXPORT\n" + instance.getClass().getName()+"#"+name +
275+
": No configurator found for type " + type);
276+
}
277+
try {
278+
Object o = getType();
279+
if (o == null) {
280+
return null;
281+
}
282+
283+
// In Export we sensitive only those values which do not get rendered as secrets
284+
boolean shouldBeMasked = isSecret(instance);
285+
if (multiple) {
286+
Sequence seq = new Sequence();
287+
if (o.getClass().isArray()) o = Arrays.asList(o);
288+
if (o instanceof Iterable) {
289+
for (Object value : (Iterable) o) {
290+
seq.add(_describe(c, context, value, shouldBeMasked));
291+
}
292+
}
293+
return seq;
294+
}
295+
return _describe(c, context, o, shouldBeMasked);
296+
} catch (Exception e) {
297+
// Don't fail the whole export, prefer logging this error
298+
LOGGER.log(Level.WARNING, "Failed to export", e);
299+
return new Scalar("FAILED TO EXPORT\n" + instance.getClass().getName() + "#" + name + ": "
300+
+ printThrowable(e));
301+
}
302+
}
303+
252304
/**
253305
* Describes a node.
254306
* @param c Configurator
@@ -261,7 +313,12 @@ public CNode describe(Owner instance, ConfigurationContext context) throws Confi
261313
*/
262314
private CNode _describe(Configurator c, ConfigurationContext context, Object value, boolean shouldBeMasked)
263315
throws Exception {
264-
CNode node = c.describe(value, context);
316+
CNode node;
317+
if (isJsonSchema) {
318+
node = c.describeStructure(value, context);
319+
} else {
320+
node = c.describe(value, context);
321+
}
265322
if (shouldBeMasked && node instanceof Scalar) {
266323
((Scalar)node).sensitive(true);
267324
}

plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationContext.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public class ConfigurationContext implements ConfiguratorRegistry {
3030

3131
private transient final ConfiguratorRegistry registry;
3232

33+
private transient String mode;
34+
3335
public ConfigurationContext(ConfiguratorRegistry registry) {
3436
this.registry = registry;
3537
}
@@ -62,6 +64,15 @@ public void setUnknown(Unknown unknown) {
6264
this.unknown = unknown;
6365
}
6466

67+
String getMode() {
68+
return mode;
69+
}
70+
71+
public void setMode(String mode) {
72+
this.mode = mode;
73+
}
74+
75+
6576

6677
// --- delegate methods for ConfigurationContext
6778

@@ -123,6 +134,11 @@ public String value() {
123134
public boolean isAtLeast(Version version) {
124135
return this.ordinal() >= version.ordinal();
125136
}
137+
138+
@Override
139+
public String toString() {
140+
return value;
141+
}
126142
}
127143

128144
static {

plugin/src/main/java/io/jenkins/plugins/casc/Configurator.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.apache.commons.lang.StringUtils;
3535
import org.jenkinsci.Symbol;
3636

37+
3738
/**
3839
* Define a {@link Configurator} which handles a configuration element, identified by name.
3940
* @author <a href="mailto:[email protected]">Nicolas De Loof</a>
@@ -167,4 +168,27 @@ default CNode describe(T instance, ConfigurationContext context) throws Exceptio
167168
return mapping;
168169
}
169170

171+
/**
172+
* Describe Structure of the attributes, as required by the schema.
173+
* @param instance
174+
* @param context
175+
* @since 1.35
176+
* @return CNode describing the attributes.
177+
*/
178+
@CheckForNull
179+
default CNode describeStructure(T instance, ConfigurationContext context)
180+
throws Exception {
181+
Mapping mapping = new Mapping();
182+
for (Attribute attribute : getAttributes()) {
183+
if (context.getMode().equals("JSONSchema")) {
184+
attribute.setJsonSchema(true);
185+
}
186+
CNode value = attribute.describeForSchema(instance, context);
187+
if (value != null) {
188+
mapping.put(attribute.getName(), attribute.getType().getSimpleName());
189+
}
190+
}
191+
return mapping;
192+
}
193+
170194
}

0 commit comments

Comments
 (0)