diff --git a/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/plugins/Manifest.java b/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/plugins/Manifest.java index 93434e10dc..6918db7ea2 100644 --- a/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/plugins/Manifest.java +++ b/halyard-config/src/main/java/com/netflix/spinnaker/halyard/config/model/v1/plugins/Manifest.java @@ -31,6 +31,7 @@ public class Manifest { public String name; public String manifestVersion; public List jars; + public List deck; public Map options; static final String regex = "^[a-zA-Z0-9]+\\/[\\w-]+$"; @@ -38,7 +39,7 @@ public class Manifest { public void validate() throws HalException { - if (Stream.of(name, manifestVersion, jars).anyMatch(Objects::isNull)) { + if (Stream.of(name, manifestVersion, jars, deck).anyMatch(Objects::isNull)) { throw new HalException( new ConfigProblemBuilder( Problem.Severity.FATAL, "Invalid plugin manifest, contains null values") diff --git a/halyard-core/src/main/java/com/netflix/spinnaker/halyard/core/resource/v1/ObjectReplaceTemplatedResource.java b/halyard-core/src/main/java/com/netflix/spinnaker/halyard/core/resource/v1/ObjectReplaceTemplatedResource.java new file mode 100644 index 0000000000..fefd64c65f --- /dev/null +++ b/halyard-core/src/main/java/com/netflix/spinnaker/halyard/core/resource/v1/ObjectReplaceTemplatedResource.java @@ -0,0 +1,51 @@ +/* + * Copyright 2019 Armory, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.netflix.spinnaker.halyard.core.resource.v1; + +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class ObjectReplaceTemplatedResource extends TemplatedResource { + public static Logger log = LoggerFactory.getLogger(ObjectReplaceTemplatedResource.class); + + private String formatKey(String key) { + return "'{{%" + key + "%}}'"; + } + + private String getRegexSafeFormatKey() { + return "(?s).*\\'\\{\\{%.*%\\}\\}\\'.*"; + } + + @Override + public String toString() { + String contents = getContents(); + for (Map.Entry binding : bindings.entrySet()) { + Object value = binding.getValue(); + contents = + contents.replace(formatKey(binding.getKey()), value != null ? value.toString() : ""); + } + if (contents.matches(getRegexSafeFormatKey())) { + log.warn( + "Found part of template that still contains a format key, likely a missing template value for a key, template: " + + contents); + } + return contents; + } +} diff --git a/halyard-core/src/main/java/com/netflix/spinnaker/halyard/core/resource/v1/ObjectResource.java b/halyard-core/src/main/java/com/netflix/spinnaker/halyard/core/resource/v1/ObjectResource.java new file mode 100644 index 0000000000..1fc5b04b09 --- /dev/null +++ b/halyard-core/src/main/java/com/netflix/spinnaker/halyard/core/resource/v1/ObjectResource.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019 Armory, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.spinnaker.halyard.core.resource.v1; + +import lombok.AccessLevel; +import lombok.Getter; + +public class ObjectResource extends ObjectReplaceTemplatedResource { + public ObjectResource(String contents) { + this.contents = contents; + } + + @Getter(AccessLevel.PROTECTED) + private final String contents; +} diff --git a/halyard-core/src/test/groovy/com/netflix/spinnaker/halyard/core/resource/v1/ObjectReplaceTemplatedResourceSpec.groovy b/halyard-core/src/test/groovy/com/netflix/spinnaker/halyard/core/resource/v1/ObjectReplaceTemplatedResourceSpec.groovy new file mode 100644 index 0000000000..a830840d5e --- /dev/null +++ b/halyard-core/src/test/groovy/com/netflix/spinnaker/halyard/core/resource/v1/ObjectReplaceTemplatedResourceSpec.groovy @@ -0,0 +1,54 @@ +/* + * Copyright 2019 Armory, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.netflix.spinnaker.halyard.core.resource.v1 + +import spock.lang.Specification + +class ObjectReplaceTemplatedResourceSpec extends Specification { + void "Attempt template with unfilled key"() { + setup: + String template = """'{{%some-unfilled-key%}}'""" + def logMock = Mock(org.slf4j.Logger) + ObjectReplaceTemplatedResource resource = (ObjectReplaceTemplatedResource)(new ObjectResource(template)) + resource.log = logMock + + when: + 1 * logMock.warn(_) >> _ + resource.toString() + + then: + true + } + + void "Attempt template with unfilled key multiline"() { + setup: + String template = """foo +'{{%some-unfilled-key%}}' +bar +""" + def logMock = Mock(org.slf4j.Logger) + ObjectReplaceTemplatedResource resource = (ObjectReplaceTemplatedResource)(new ObjectResource(template)) + resource.log = logMock + + when: + 1 * logMock.warn(_) >> _ + resource.toString() + + then: + true + } +} diff --git a/halyard-deploy/src/main/java/com/netflix/spinnaker/halyard/deploy/spinnaker/v1/profile/deck/DeckProfileFactory.java b/halyard-deploy/src/main/java/com/netflix/spinnaker/halyard/deploy/spinnaker/v1/profile/deck/DeckProfileFactory.java index c2804c860c..e1773545c4 100644 --- a/halyard-deploy/src/main/java/com/netflix/spinnaker/halyard/deploy/spinnaker/v1/profile/deck/DeckProfileFactory.java +++ b/halyard-deploy/src/main/java/com/netflix/spinnaker/halyard/deploy/spinnaker/v1/profile/deck/DeckProfileFactory.java @@ -17,6 +17,7 @@ package com.netflix.spinnaker.halyard.deploy.spinnaker.v1.profile.deck; +import com.google.gson.Gson; import com.netflix.spinnaker.halyard.config.model.v1.canary.Canary; import com.netflix.spinnaker.halyard.config.model.v1.node.DeploymentConfiguration; import com.netflix.spinnaker.halyard.config.model.v1.node.Features; @@ -24,6 +25,8 @@ import com.netflix.spinnaker.halyard.config.model.v1.notifications.GithubStatusNotification; import com.netflix.spinnaker.halyard.config.model.v1.notifications.SlackNotification; import com.netflix.spinnaker.halyard.config.model.v1.notifications.TwilioNotification; +import com.netflix.spinnaker.halyard.config.model.v1.plugins.Manifest; +import com.netflix.spinnaker.halyard.config.model.v1.plugins.Plugin; import com.netflix.spinnaker.halyard.config.model.v1.providers.appengine.AppengineProvider; import com.netflix.spinnaker.halyard.config.model.v1.providers.aws.AwsAccount; import com.netflix.spinnaker.halyard.config.model.v1.providers.aws.AwsProvider; @@ -37,16 +40,15 @@ import com.netflix.spinnaker.halyard.config.services.v1.AccountService; import com.netflix.spinnaker.halyard.config.services.v1.VersionsService; import com.netflix.spinnaker.halyard.core.registry.v1.Versions; +import com.netflix.spinnaker.halyard.core.resource.v1.ObjectResource; import com.netflix.spinnaker.halyard.core.resource.v1.StringResource; import com.netflix.spinnaker.halyard.deploy.spinnaker.v1.SpinnakerArtifact; import com.netflix.spinnaker.halyard.deploy.spinnaker.v1.SpinnakerRuntimeSettings; import com.netflix.spinnaker.halyard.deploy.spinnaker.v1.profile.Profile; import com.netflix.spinnaker.halyard.deploy.spinnaker.v1.profile.RegistryBackedProfileFactory; import com.netflix.spinnaker.halyard.deploy.spinnaker.v1.service.SpinnakerService.Type; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; +import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -72,7 +74,11 @@ protected void setProfile( Profile profile, DeploymentConfiguration deploymentConfiguration, SpinnakerRuntimeSettings endpoints) { - StringResource configTemplate = new StringResource(profile.getBaseContents()); + ObjectResource configTemplate = + new ObjectResource( + profile + .getBaseContents() + .replace("version: version,", "version: version,\n plugins: '{{%plugins%}}',")); UiSecurity uiSecurity = deploymentConfiguration.getSecurity().getUiSecurity(); profile.setUser(ApacheSettings.APACHE_USER); @@ -232,8 +238,35 @@ protected void setProfile( bindings.put("canary.showAllCanaryConfigs", canary.isShowAllConfigsEnabled()); } + // Configure Plugins + List pluginBinding = new ArrayList<>(); + List plugins = deploymentConfiguration.getPlugins().getPlugins(); + Map> pluginMetadata = + plugins.stream() + .filter(Plugin::getEnabled) + .filter(p -> !p.getManifestLocation().isEmpty()) + .map(Plugin::generateManifest) + .collect(Collectors.toMap(Manifest::getName, Manifest::getDeck)); + for (Map.Entry> entry : pluginMetadata.entrySet()) { + for (String location : entry.getValue()) { + Map resource = new HashMap<>(); + resource.put("name", entry.getKey()); + resource.put("location", location); + String extension = location.substring(location.lastIndexOf(".")); + if (extension == "css") { + resource.put("type", "css"); + } else { + resource.put("type", "js"); + } + pluginBinding.add(new Gson().toJson(resource)); + } + } + bindings.put("plugins", pluginBinding); + + StringResource objectConfigTemplate = + new StringResource(configTemplate.setBindings(bindings).toString()); profile - .appendContents(configTemplate.setBindings(bindings).toString()) + .appendContents(objectConfigTemplate.setBindings(bindings).toString()) .setRequiredFiles(backupRequiredFiles(uiSecurity, deploymentConfiguration.getName())); } }