Skip to content
Permalink
Browse files
Merge pull request #30 from oleg-nenashev/casc-support
[JENKINS-51656] - Add support of Configuration-as-Code Plugin to Custom WAR Packager
  • Loading branch information
oleg-nenashev committed Jun 7, 2018
2 parents 6e11f71 + 2157dc3 commit 77b392db0198d73d08b90398aab180c3aff24a83
@@ -13,14 +13,17 @@ Differences:
* It can run as a CLI tool outside Maven
* It takes YAML specification instead of Maven `pom.xml`
* It allows patching WAR contents like bundled libraries, system properties
* It allows embedding [Groovy Hook Scripts](https://wiki.jenkins.io/display/JENKINS/Groovy+Hook+Script)
so that the instance can be auto-configured
* It allows self-configuration via [Groovy Hook Scripts](https://wiki.jenkins.io/display/JENKINS/Groovy+Hook+Script)
or [Configuration-as-Code Plugin](https://github.com/jenkinsci/configuration-as-code-plugin) YAML files

### Demo

* [Jenkins WAR - all latest](./demo/all-latest-core) - bundles master branches for core and some key libraries/modules
* [Jenkins WAR - all latest with Maven](./demo/all-latest-core-maven) - same as a above, but with Maven
* [External Task Logging to Elasticsearch](./demo/external-logging-elasticsearch)
* [External Task Logging to Elasticsearch](./demo/external-logging-elasticsearch) -
runs External Logging demo and preconfigures it using System Groovy Hooks
* [Configuration as Code](./demo/casc) - configuring WAR with
[Configuration-as-Code Plugin](https://github.com/jenkinsci/configuration-as-code-plugin) via YAML
* [Custom WAR Packager CI Demo](https://github.com/oleg-nenashev/jenkins-custom-war-packager-ci-demo) - Standalone demo with an integrated CI flow

### Usage
@@ -0,0 +1,21 @@
package io.jenkins.tools.warpackager.lib.config;

/**
* Provides integration with Configuration-as-Code plugin.
* @author Oleg Nenashev
* @since TODO
*/
public class CasCConfig extends WARResourceInfo {

public static final String CASC_PLUGIN_ARTIFACT_ID = "configuration-as-code";

@Override
public String getDestination() {
return "WEB-INF/jenkins.yaml.d";
}

@Override
public String getResourceType() {
return "jenkins.yaml";
}
}
@@ -18,6 +18,7 @@
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -45,6 +46,8 @@ public class Config {
public Map<String, String> systemProperties;
@CheckForNull
public Collection<GroovyHookInfo> groovyHooks;
@CheckForNull
public Collection<CasCConfig> casc;

private static Config load(@Nonnull InputStream istream, boolean isEssentialsYML) throws IOException {
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
@@ -123,6 +126,27 @@ public GroovyHookInfo getHookById(@Nonnull String id) {
return null;
}

public List<WARResourceInfo> getAllExtraResources() {
final List<WARResourceInfo> list = new ArrayList<>();
if (groovyHooks != null) {
list.addAll(groovyHooks);
}
if (casc != null) {
list.addAll(casc);
}
return list;
}

@CheckForNull
public WARResourceInfo findResourceById(@Nonnull String id) {
for (WARResourceInfo hook : getAllExtraResources()) {
if (id.equals(hook.id)) {
return hook;
}
}
return null;
}

@CheckForNull
public DependencyInfo findPlugin(@Nonnull String artifactId) {
if (plugins == null) {
@@ -7,8 +7,16 @@
* @author Oleg Nenashev
*/
@SuppressFBWarnings(value = "UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", justification = "Comes from YAML")
public class GroovyHookInfo {
public class GroovyHookInfo extends WARResourceInfo {
public String type;
public String id;
public SourceInfo source;

@Override
public String getResourceType() {
return "groovy-hooks." + type;
}

@Override
public String getDestination() {
return "WEB-INF/" + type + ".groovy.d";
}
}
@@ -0,0 +1,21 @@
package io.jenkins.tools.warpackager.lib.config;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
* Abstraction for all resources being injected into WARs.
* @author Oleg Nenashev
* @since TODO
*/
@SuppressFBWarnings(value = "UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", justification = "JSON Deserialization")
public abstract class WARResourceInfo {
public String id;
public SourceInfo source;

public abstract String getResourceType();

/**
* Gets relative path to the resource within WAR.
*/
public abstract String getDestination();
}
@@ -4,6 +4,7 @@
import io.jenkins.tools.warpackager.lib.config.DependencyInfo;
import io.jenkins.tools.warpackager.lib.config.GroovyHookInfo;
import io.jenkins.tools.warpackager.lib.config.SourceInfo;
import io.jenkins.tools.warpackager.lib.config.WARResourceInfo;
import io.jenkins.tools.warpackager.lib.model.bom.BOM;
import io.jenkins.tools.warpackager.lib.model.bom.ComponentReference;
import io.jenkins.tools.warpackager.lib.model.bom.Metadata;
@@ -18,7 +19,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;

@@ -146,8 +146,8 @@ private Specification buildSpec(boolean overrideVersions) {
}
}
if (config.groovyHooks != null) {
for (GroovyHookInfo hookInfo : config.groovyHooks) {
components.add(toComponentReference(hookInfo, overrideVersions));
for (WARResourceInfo extraResource : config.getAllExtraResources()) {
components.add(toComponentReference(extraResource, overrideVersions));
}
}
spec.setComponents(components);
@@ -175,10 +175,10 @@ private ComponentReference toComponentReference(DependencyInfo dep, boolean over
return ref;
}

private ComponentReference toComponentReference(GroovyHookInfo hook, boolean overrideVersions) {
private ComponentReference toComponentReference(WARResourceInfo hook, boolean overrideVersions) {
//TODO(oleg_nenashev): no artifact IDs, some hacks here. Maybe groovy hooks should require standard fields
DependencyInfo mockDependency = new DependencyInfo();
mockDependency.groupId = "io.jenkins.tools.warpackager.hooks." + hook.type;
mockDependency.groupId = "io.jenkins.tools.warpackager." + hook.getResourceType() + "." + hook.id;
mockDependency.artifactId = hook.id;
mockDependency.source = hook.source;

@@ -1,9 +1,10 @@
package io.jenkins.tools.warpackager.lib.impl;

import io.jenkins.tools.warpackager.lib.config.CasCConfig;
import io.jenkins.tools.warpackager.lib.config.Config;
import io.jenkins.tools.warpackager.lib.config.DependencyInfo;
import io.jenkins.tools.warpackager.lib.config.GroovyHookInfo;
import io.jenkins.tools.warpackager.lib.config.SourceInfo;
import io.jenkins.tools.warpackager.lib.config.WARResourceInfo;
import io.jenkins.tools.warpackager.lib.model.bom.BOM;
import io.jenkins.tools.warpackager.lib.util.SimpleManifest;
import org.apache.maven.model.Model;
@@ -44,8 +45,23 @@ public Builder(Config config) {
this.buildRoot = new File(config.buildSettings.getTmpDir(), "build");
}

/**
* Performs spot-check of the input configuration.
* It does not guarantee that the configuration is fully correct.
*/
public void verifyConfig() throws IOException {
if (config.casc != null && !config.casc.isEmpty()) {
DependencyInfo dep = config.findPlugin(CasCConfig.CASC_PLUGIN_ARTIFACT_ID);
if (dep == null) {
throw new IOException("CasC section is declared, but CasC plugin is not declared in the plugins list");
}
}
}

public void build() throws IOException, InterruptedException {

verifyConfig();

// Cleanup the temporary directory
final File tmpDir = config.buildSettings.getTmpDir();

@@ -83,12 +99,11 @@ public void build() throws IOException, InterruptedException {
}
}

// Prepare Groovy Hooks
Map<String, File> hooks = new HashMap<>();
if (config.groovyHooks != null) {
for (GroovyHookInfo hook : config.groovyHooks) {
hooks.put(hook.id, checkoutIfNeeded(hook.id, hook.source));
}
// Prepare Resources
Map<String, File> warResources = new HashMap<>();
for (WARResourceInfo extraWarResource : config.getAllExtraResources()) {
warResources.put(extraWarResource.id,
checkoutIfNeeded(extraWarResource.id, extraWarResource.source));
}

// Generate POM
@@ -111,7 +126,7 @@ public void build() throws IOException, InterruptedException {
.addSystemProperties(config.systemProperties)
.replaceLibs(versionOverrides)
.excludeLibs()
.addHooks(hooks);
.addResources(warResources);

File warOutputDir = config.buildSettings.getOutputDir();
SimpleManifest manifest = SimpleManifest.parseFile(srcWar);
@@ -4,8 +4,7 @@

import io.jenkins.tools.warpackager.lib.config.Config;
import io.jenkins.tools.warpackager.lib.config.DependencyInfo;
import io.jenkins.tools.warpackager.lib.config.GroovyHookInfo;
import io.jenkins.tools.warpackager.lib.util.MavenHelper;
import io.jenkins.tools.warpackager.lib.config.WARResourceInfo;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
@@ -189,21 +188,21 @@ private void excludeLib(DependencyInfo lib) throws IOException, InterruptedExcep
}
}

public JenkinsWarPatcher addHooks(@Nonnull Map<String, File> hooks) throws IOException {
for (Map.Entry<String, File> hookSrc : hooks.entrySet()) {
final String hookId = hookSrc.getKey();
GroovyHookInfo hook = config.getHookById(hookId);
if (hook == null) {
throw new IOException("Cannot find metadata for the hook with ID=" + hookId);
public JenkinsWarPatcher addResources(@Nonnull Map<String, File> resources) throws IOException {
for (Map.Entry<String, File> resourceSrc : resources.entrySet()) {
final String resourceId = resourceSrc.getKey();
WARResourceInfo resource = config.findResourceById(resourceId);
if (resource == null) {
throw new IOException("Cannot find metadata for the resource with ID=" + resourceId);
}
addHook(hook, hookSrc.getValue());
addResource(resource, resourceSrc.getValue());
}

return this;
}

public void addHook(@Nonnull GroovyHookInfo hook, File path) throws IOException {
File targetDir = new File(dstDir, "WEB-INF/" + hook.type + ".groovy.d");
public void addResource(@Nonnull WARResourceInfo resource, File path) throws IOException {
File targetDir = new File(dstDir, resource.getDestination());
if (!targetDir.exists()) {
Files.createDirectories(targetDir.toPath());
}
@@ -0,0 +1,34 @@
# Just a Makefile for manual testing
.PHONY: all

ARTIFACT_ID = jenkins-casc-demo
VERSION = 256.0-test

all: clean build

clean:
rm -rf tmp

build: tmp/output/target/${ARTIFACT_ID}-${VERSION}.war

tmp/output/target/${ARTIFACT_ID}-${VERSION}.war:
java \
-jar $(shell ls ../../custom-war-packager-cli/target/custom-war-packager-cli-*-jar-with-dependencies.jar) \
-configPath packager-config.yml -version ${VERSION}

run: tmp/output/target/${ARTIFACT_ID}-${VERSION}.war
JENKINS_HOME=work java \
-jar tmp/output/target/${ARTIFACT_ID}-${VERSION}.war \
--httpPort=8080 --prefix=/jenkins

pct: tmp/output/target/${ARTIFACT_ID}-${VERSION}.war
docker run --rm \
-v ${HOME}/.m2:/root/.m2 \
-v $(shell pwd)/pct_out:/pct/out \
-v $(shell pwd)/pct_tmp:/pct/tmp \
-v $(shell pwd)/tmp/output/target/${ARTIFACT_ID}-${VERSION}.war:/pct/jenkins.war:ro \
-e ARTIFACT_ID=role-strategy \
-e INSTALL_BUNDLED_SNAPSHOTS=true \
jenkins/pct \
-mavenProperties jenkins-test-harness.version=2.38:jth.jenkins-war.path=/pct/jenkins.war

@@ -0,0 +1,11 @@
Custom WAR Packager. Configuration-as-Code demo
===

This demo demonstrates usage of Configuration-as-Code plugin together
with Jenkins Custom WAR Packager.

Use the provided Makefile in order to build (`make clean build`)
and run (`make run`) the demo.
The root Custom WAR Packager project should be built first before running.


@@ -0,0 +1,70 @@
jenkins:
systemMessage: "Custom War Packager - Demo of Configuration-as-Code plugin"
securityRealm:
local:
allowsSignup: false
users:
- id: "admin"
password: "admin"
- id: "user"
password: "user"

# Ownership-based Security configuration sample.
# It requires RoleStrategy and Ownership plugins to be installed.
authorizationStrategy:
roleStrategy:
roles:
global:
- name: "admin"
description: "Jenkins administrators"
permissions:
- "Overall/Administer"
assignments:
- "admin"
- name: "readonly"
description: "Read-only users"
permissions:
- "Overall/Read"
- "Job/Read"
- "Agent/Build"
assignments:
- "authenticated"
items:
- name: "@OwnerNoSid"
description: "Primary Owners"
pattern: ".*"
permissions:
- "Job/Configure"
- "Job/Build"
- "Job/Delete"
- "Run/Delete"
assignments:
- "authenticated"
- name: "@CoOwnerNoSid"
description: "Secondary Owners"
pattern: ".*"
permissions:
- "Job/Configure"
- "Job/Build"
assignments:
- "authenticated"
agents:
- name: "@OwnerNoSid"
description: "Primary Owners"
pattern: ".*"
permissions:
- "Agent/Configure"
- "Agent/Build"
- "Agent/Delete"
- "Agent/Build"
assignments:
- "authenticated"
- name: "@CoOwnerNoSid"
description: "Secondary Owners"
pattern: ".*"
permissions:
- "Agent/Connect"
- "Agent/Build"
assignments:
- "authenticated"

0 comments on commit 77b392d

Please sign in to comment.