From 400f29212e1f6437ff42a81c70ccac7e65b4da96 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 19 Nov 2025 17:32:01 +0530 Subject: [PATCH 1/5] feature: import extension using yaml manifest Adds import functionality for extension by giving an YAML manifest URL. Manifest will define extension and its custom actions details. Co-authored-by: Manoj Kumar Signed-off-by: Abhishek Kumar --- .../apache/cloudstack/api/ApiConstants.java | 1 + framework/extensions/pom.xml | 5 + .../extensions/api/ImportExtensionCmd.java | 96 +++++++++++++ .../manager/ExtensionsImportManager.java | 25 ++++ .../manager/ExtensionsImportManagerImpl.java | 130 ++++++++++++++++++ .../extensions/manager/ExtensionsManager.java | 7 + .../manager/ExtensionsManagerImpl.java | 64 ++++++--- .../extensions/util/ExtensionConfig.java | 128 +++++++++++++++++ .../framework/extensions/util/YamlParser.java | 24 ++++ .../extensions/util/ZipExtractor.java | 87 ++++++++++++ ...ring-framework-extensions-core-context.xml | 1 + .../extensions/util/YamlParserTest.java | 31 +++++ .../extensions/util/ZipExtractorTest.java | 17 +++ .../framework/extensions/util/manifest.yaml | 95 +++++++++++++ ui/public/locales/en.json | 2 + ui/src/config/section/extension.js | 9 ++ ui/src/views/extension/ImportExtension.vue | 116 ++++++++++++++++ 17 files changed, 817 insertions(+), 21 deletions(-) create mode 100644 framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/ImportExtensionCmd.java create mode 100644 framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsImportManager.java create mode 100644 framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsImportManagerImpl.java create mode 100644 framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/util/ExtensionConfig.java create mode 100644 framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/util/YamlParser.java create mode 100644 framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/util/ZipExtractor.java create mode 100644 framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/util/YamlParserTest.java create mode 100644 framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/util/ZipExtractorTest.java create mode 100644 framework/extensions/src/test/resources/org/apache/cloudstack/framework/extensions/util/manifest.yaml create mode 100644 ui/src/views/extension/ImportExtension.vue diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 8fca652518f2..54c07008880f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -374,6 +374,7 @@ public class ApiConstants { public static final String LB_PROVIDER = "lbprovider"; public static final String MAC_ADDRESS = "macaddress"; public static final String MAC_ADDRESSES = "macaddresses"; + public static final String MANIFEST_URL = "manifesturl"; public static final String MANUAL_UPGRADE = "manualupgrade"; public static final String MAX = "max"; public static final String MAX_SNAPS = "maxsnaps"; diff --git a/framework/extensions/pom.xml b/framework/extensions/pom.xml index 4d42d6840ddf..7196e5710a67 100644 --- a/framework/extensions/pom.xml +++ b/framework/extensions/pom.xml @@ -50,5 +50,10 @@ 4.23.0.0-SNAPSHOT compile + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + ${cs.jackson.version} + diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/ImportExtensionCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/ImportExtensionCmd.java new file mode 100644 index 000000000000..7999a75590f0 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/ImportExtensionCmd.java @@ -0,0 +1,96 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 org.apache.cloudstack.framework.extensions.api; + +import java.util.EnumSet; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.ExtensionResponse; +import org.apache.cloudstack.extension.Extension; +import org.apache.cloudstack.framework.extensions.manager.ExtensionsImportManager; +import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.user.Account; + +@APICommand(name = "importExtension", + description = "Imports an extension", + responseObject = ExtensionResponse.class, + responseHasSensitiveInfo = false, + entityType = {Extension.class}, + authorized = {RoleType.Admin}, + since = "4.23.0") +public class ImportExtensionCmd extends BaseCmd { + + @Inject + ExtensionsManager extensionsManager; + + @Inject + ExtensionsImportManager extensionsImportManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.MANIFEST_URL, type = CommandType.STRING, required = true, + description = "URL of the extension manifest import file") + private String manifestUrl; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getManifestUrl() { + return manifestUrl; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + Extension extension = extensionsImportManager.importExtension(this); + ExtensionResponse response = extensionsManager.createExtensionResponse(extension, + EnumSet.of(ApiConstants.ExtensionDetails.all)); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.Extension; + } + + +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsImportManager.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsImportManager.java new file mode 100644 index 000000000000..c09888a5299b --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsImportManager.java @@ -0,0 +1,25 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 org.apache.cloudstack.framework.extensions.manager; + +import org.apache.cloudstack.extension.Extension; +import org.apache.cloudstack.framework.extensions.api.ImportExtensionCmd; + +public interface ExtensionsImportManager { + Extension importExtension(ImportExtensionCmd cmd); +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsImportManagerImpl.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsImportManagerImpl.java new file mode 100644 index 000000000000..f8ddcdd876f9 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsImportManagerImpl.java @@ -0,0 +1,130 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 org.apache.cloudstack.framework.extensions.manager; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.inject.Inject; + +import org.apache.cloudstack.extension.Extension; +import org.apache.cloudstack.framework.extensions.api.ImportExtensionCmd; +import org.apache.cloudstack.framework.extensions.dao.ExtensionDao; +import org.apache.cloudstack.framework.extensions.util.ExtensionConfig; +import org.apache.cloudstack.framework.extensions.util.YamlParser; +import org.apache.cloudstack.framework.extensions.util.ZipExtractor; +import org.apache.cloudstack.framework.extensions.vo.ExtensionVO; +import org.apache.commons.lang3.StringUtils; + +import com.cloud.hypervisor.ExternalProvisioner; +import com.cloud.utils.HttpUtils; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallbackWithException; +import com.cloud.utils.exception.CloudRuntimeException; + +public class ExtensionsImportManagerImpl extends ManagerBase implements ExtensionsImportManager { + + @Inject + ExtensionsManager extensionsManager; + + @Inject + ExternalProvisioner externalProvisioner; + + @Inject + ExtensionDao extensionDao; + + protected Extension importExtensionInternal(String manifestUrl, Path tempDir) { + Path manifestPath = tempDir.resolve("manifest.yaml"); + HttpUtils.downloadFileWithProgress(manifestUrl, manifestPath.toString(), logger); + if (!Files.exists(manifestPath)) { + throw new CloudRuntimeException("Failed to download extension manifest from URL: " + manifestUrl); + } + final ExtensionConfig extensionConfig = YamlParser.parseYamlFile(manifestPath.toString()); + //Parse the manifest and create the extension + final String name = extensionConfig.metadata.name; + final String extensionArchiveURL = extensionConfig.getArchiveUrl(); + ExtensionVO extensionByName = extensionDao.findByName(name); + if (extensionByName != null) { + throw new CloudRuntimeException("Extension by name already exists"); + } + if (StringUtils.isBlank(extensionArchiveURL)) { + throw new CloudRuntimeException("Unable to retrieve archive URL for extension source during import"); + } + Path extensionArchivePath = tempDir.resolve(UUID.randomUUID() + ".zip"); + HttpUtils.downloadFileWithProgress(extensionArchiveURL, extensionArchivePath.toString(), logger); + if (!Files.exists(extensionArchivePath)) { + throw new CloudRuntimeException("Failed to download extension archive from URL: " + extensionArchiveURL); + } + final String extensionRootPath = externalProvisioner.getExtensionsPath() + File.separator + name; + try { + ZipExtractor.extractZipContents(extensionArchivePath.toString(), extensionRootPath); + } catch (IOException e) { + throw new CloudRuntimeException("Failed to extract extension archive during import at: " + extensionRootPath, e); + } + return Transaction.execute((TransactionCallbackWithException) status -> { + Extension extension = extensionsManager.createExtension(name, extensionConfig.metadata.description, + extensionConfig.spec.type, extensionConfig.spec.entrypoint.path, Extension.State.Enabled.name(), + false, Collections.emptyMap()); + + for (ExtensionConfig.CustomAction action : extensionConfig.spec.customActions) { + List> parameters = action.getParametersMapList(); + Map>> parametersMap = new HashMap<>(); + parametersMap.put(1, parameters); + extensionsManager.addCustomAction(action.name, action.description, extension.getId(), + action.resourcetype, action.allowedroletypes, action.timeout, true, parametersMap, + null, null, Collections.emptyMap()); + } + return null; + }); + } + + @Override + public Extension importExtension(ImportExtensionCmd cmd) { + final String manifestUrl = cmd.getManifestUrl(); + final String extensionsRootPath = externalProvisioner.getExtensionsPath(); + + Path tempDir; + try { + Path extensionsRootDir = Paths.get(extensionsRootPath); + Files.createDirectories(extensionsRootDir); + tempDir = Files.createTempDirectory(extensionsRootDir, "import-ext-"); + + } catch (IOException e) { + logger.error("Failed to create working directory for import extension, {}", extensionsRootPath, e); + throw new CloudRuntimeException("Failed to create working directory for import extension", e); + } + try { + return importExtensionInternal(manifestUrl, tempDir); + } catch (Exception e) { + logger.error(e.getMessage(), e); + throw e; + }/* finally { + FileUtil.deletePath(tempDir.toString()); + }*/ + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java index 1b1a175c5975..e50f0fa61db9 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java @@ -57,6 +57,9 @@ public interface ExtensionsManager extends Manager { Extension createExtension(CreateExtensionCmd cmd); + Extension createExtension(String name, String description, String type, String relativePath, String state, + Boolean orchestratorRequiresPrepareVm, Map details); + boolean prepareExtensionPathAcrossServers(Extension extension); List listExtensions(ListExtensionsCmd cmd); @@ -79,6 +82,10 @@ public interface ExtensionsManager extends Manager { ExtensionCustomAction addCustomAction(AddCustomActionCmd cmd); + ExtensionCustomAction addCustomAction(String name, String description, long extensionId, String resourceTypeStr, + List rolesStrList, int timeout , boolean enabled, Map parametersMap, String successMessage, + String errorMessage, Map details); + boolean deleteCustomAction(DeleteCustomActionCmd cmd); List listCustomActions(ListCustomActionCmd cmd); diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java index 4171b9615fea..7c711d885476 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java @@ -68,6 +68,7 @@ import org.apache.cloudstack.framework.extensions.api.CreateExtensionCmd; import org.apache.cloudstack.framework.extensions.api.DeleteCustomActionCmd; import org.apache.cloudstack.framework.extensions.api.DeleteExtensionCmd; +import org.apache.cloudstack.framework.extensions.api.ImportExtensionCmd; import org.apache.cloudstack.framework.extensions.api.ListCustomActionCmd; import org.apache.cloudstack.framework.extensions.api.ListExtensionsCmd; import org.apache.cloudstack.framework.extensions.api.RegisterExtensionCmd; @@ -570,13 +571,8 @@ public String getExtensionsPath() { @Override @ActionEvent(eventType = EventTypes.EVENT_EXTENSION_CREATE, eventDescription = "creating extension") - public Extension createExtension(CreateExtensionCmd cmd) { - final String name = cmd.getName(); - final String description = cmd.getDescription(); - final String typeStr = cmd.getType(); - String relativePath = cmd.getPath(); - final Boolean orchestratorRequiresPrepareVm = cmd.isOrchestratorRequiresPrepareVm(); - final String stateStr = cmd.getState(); + public Extension createExtension(String name, String description, String typeStr, String relativePath, + String stateStr, Boolean orchestratorRequiresPrepareVm, Map details) { ExtensionVO extensionByName = extensionDao.findByName(name); if (extensionByName != null) { throw new CloudRuntimeException("Extension by name already exists"); @@ -612,7 +608,6 @@ public Extension createExtension(CreateExtensionCmd cmd) { } extension = extensionDao.persist(extension); - Map details = cmd.getDetails(); List detailsVOList = new ArrayList<>(); if (MapUtils.isNotEmpty(details)) { for (Map.Entry entry : details.entrySet()) { @@ -640,6 +635,22 @@ public Extension createExtension(CreateExtensionCmd cmd) { return extensionVO; } + + + @Override + @ActionEvent(eventType = EventTypes.EVENT_EXTENSION_CREATE, eventDescription = "creating extension") + public Extension createExtension(CreateExtensionCmd cmd) { + final String name = cmd.getName(); + final String description = cmd.getDescription(); + final String typeStr = cmd.getType(); + String relativePath = cmd.getPath(); + final Boolean orchestratorRequiresPrepareVm = cmd.isOrchestratorRequiresPrepareVm(); + final String stateStr = cmd.getState(); + final Map details = cmd.getDetails(); + return createExtension(name, description, typeStr, relativePath, stateStr, orchestratorRequiresPrepareVm, + details); + } + @Override public boolean prepareExtensionPathAcrossServers(Extension extension) { boolean prepared = true; @@ -973,18 +984,10 @@ public ExtensionResponse createExtensionResponse(Extension extension, @Override @ActionEvent(eventType = EventTypes.EVENT_EXTENSION_CUSTOM_ACTION_ADD, eventDescription = "adding extension custom action") - public ExtensionCustomAction addCustomAction(AddCustomActionCmd cmd) { - String name = cmd.getName(); - String description = cmd.getDescription(); - Long extensionId = cmd.getExtensionId(); - String resourceTypeStr = cmd.getResourceType(); - List rolesStrList = cmd.getAllowedRoleTypes(); - final int timeout = ObjectUtils.defaultIfNull(cmd.getTimeout(), 3); - final boolean enabled = cmd.isEnabled(); - Map parametersMap = cmd.getParametersMap(); - final String successMessage = cmd.getSuccessMessage(); - final String errorMessage = cmd.getErrorMessage(); - Map details = cmd.getDetails(); + public ExtensionCustomAction addCustomAction(String name, String description, long extensionId, + String resourceTypeStr, List rolesStrList, int timeout , boolean enabled, Map parametersMap, + String successMessage, String errorMessage, Map details) { + if (name == null || !name.matches("^[a-zA-Z0-9 _-]+$")) { throw new InvalidParameterValueException(String.format("Invalid action name: %s. It can contain " + "only alphabets, numbers, hyphen, underscore and space", name)); @@ -1004,7 +1007,7 @@ public ExtensionCustomAction addCustomAction(AddCustomActionCmd cmd) { if (resourceType == null) { throw new InvalidParameterValueException( String.format("Invalid resource type specified: %s. Valid values are: %s", resourceTypeStr, - EnumSet.allOf(ExtensionCustomAction.ResourceType.class))); + EnumSet.allOf(ExtensionCustomAction.ResourceType.class))); } } if (resourceType == null && Extension.Type.Orchestrator.equals(extensionVO.getType())) { @@ -1050,6 +1053,24 @@ public ExtensionCustomAction addCustomAction(AddCustomActionCmd cmd) { }); } + @Override + @ActionEvent(eventType = EventTypes.EVENT_EXTENSION_CUSTOM_ACTION_ADD, eventDescription = "adding extension custom action") + public ExtensionCustomAction addCustomAction(AddCustomActionCmd cmd) { + String name = cmd.getName(); + String description = cmd.getDescription(); + Long extensionId = cmd.getExtensionId(); + String resourceTypeStr = cmd.getResourceType(); + List rolesStrList = cmd.getAllowedRoleTypes(); + final int timeout = ObjectUtils.defaultIfNull(cmd.getTimeout(), 3); + final boolean enabled = cmd.isEnabled(); + Map parametersMap = cmd.getParametersMap(); + final String successMessage = cmd.getSuccessMessage(); + final String errorMessage = cmd.getErrorMessage(); + Map details = cmd.getDetails(); + return addCustomAction(name, description, extensionId, resourceTypeStr, rolesStrList, timeout, enabled, + parametersMap, successMessage, errorMessage, details); + } + @Override @ActionEvent(eventType = EventTypes.EVENT_EXTENSION_CUSTOM_ACTION_DELETE, eventDescription = "deleting extension custom action") public boolean deleteCustomAction(DeleteCustomActionCmd cmd) { @@ -1637,6 +1658,7 @@ public List> getCommands() { cmds.add(RunCustomActionCmd.class); cmds.add(CreateExtensionCmd.class); + cmds.add(ImportExtensionCmd.class); cmds.add(ListExtensionsCmd.class); cmds.add(DeleteExtensionCmd.class); cmds.add(UpdateExtensionCmd.class); diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/util/ExtensionConfig.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/util/ExtensionConfig.java new file mode 100644 index 000000000000..03e75e9346d2 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/util/ExtensionConfig.java @@ -0,0 +1,128 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 org.apache.cloudstack.framework.extensions.util; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ExtensionConfig { + + public String apiVersion; + public String kind; + public Metadata metadata; + public Source source; + public Spec spec; + + String archiveUrl; + + // ----------------------------- + // Nested compact model + // ----------------------------- + + public static class Metadata { + public String name; + public String displayName; + public String description; + public String version; + public String maintainer; + public String homepage; + } + + public static class Source { + public String type; + public String url; + public String refs; + } + + public static class Spec { + public String type; + public Compatibility compatibility; + public Entrypoint entrypoint; + public Orchestrator orchestrator; + private Map details; + public boolean enabled; + public List customActions; + + public Map getDetails() { + return details; + } + + public void setDetails(Map details) { + this.details = details; + } + } + + public static class Compatibility { + public CloudStack cloudstack; + } + + public static class CloudStack { + public String minVersion; + } + + public static class Entrypoint { + public String language; + public String path; + public String targetDir; + } + + public static class Orchestrator { + public boolean requiresPrepareVm; + } + + public static class CustomAction { + public String name; + public String displayName; + public String description; + public String resourcetype; + public boolean enabled; + public int timeout; + public List allowedroletypes; + public List parameters; + + public List> getParametersMapList() { + return parameters.stream().map(param -> { + Map paramMap = new java.util.HashMap<>(); + paramMap.put("name", param.name); + paramMap.put("type", param.type); + paramMap.put("validationformat", param.validationformat); + paramMap.put("required", Boolean.toString(param.required)); + return paramMap; + }).collect(Collectors.toList()); + } + } + + public static class Parameter { + public String name; + public String type; + public String validationformat; + public boolean required; + } + + public String getArchiveUrl() { + return source.url + "archive/refs/heads/" + source.refs + ".zip"; + } + + public Spec getSpec() { + return spec; + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/util/YamlParser.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/util/YamlParser.java new file mode 100644 index 000000000000..8d226d1fc00f --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/util/YamlParser.java @@ -0,0 +1,24 @@ +package org.apache.cloudstack.framework.extensions.util; + + +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +import com.cloud.utils.exception.CloudRuntimeException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +public class YamlParser { + private static final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + + public static ExtensionConfig parseYamlFile(String filePath) { + ExtensionConfig extensionConfig = null; + try (InputStream in = Files.newInputStream(Path.of(filePath))) { + extensionConfig = mapper.readValue(in, ExtensionConfig.class); + } catch (Exception ex) { + throw new CloudRuntimeException("Failed to parse YAML", ex); + } + return extensionConfig; + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/util/ZipExtractor.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/util/ZipExtractor.java new file mode 100644 index 000000000000..e6d559e8fae8 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/util/ZipExtractor.java @@ -0,0 +1,87 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 org.apache.cloudstack.framework.extensions.util; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class ZipExtractor { + + /** + * Extracts a GitHub ZIP file contents directly into destDir, skipping top-level folder. + * + * @param zipFilePath Path to the ZIP file + * @param destDir Destination directory + */ + public static void extractZipContents(String zipFilePath, String destDir) throws IOException { + Path destPath = Paths.get(destDir); + if (!Files.exists(destPath)) { + Files.createDirectories(destPath); + } + + try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(Paths.get(zipFilePath)))) { + ZipEntry entry; + + while ((entry = zis.getNextEntry()) != null) { + String entryName = entry.getName(); + + // Skip the top-level folder (everything before first '/') + int firstSlash = entryName.indexOf('/'); + if (firstSlash >= 0) { + entryName = entryName.substring(firstSlash + 1); + } + + if (entryName.isEmpty()) { + zis.closeEntry(); + continue; // skip the top-level folder itself + } + + Path newPath = safeResolve(destPath, entryName); + + if (entry.isDirectory()) { + Files.createDirectories(newPath); + } else { + if (newPath.getParent() != null) { + Files.createDirectories(newPath.getParent()); + } + try (OutputStream os = Files.newOutputStream(newPath)) { + zis.transferTo(os); + } + } + + zis.closeEntry(); + } + } + } + + /** + * Protects from ZIP Slip vulnerability. + */ + private static Path safeResolve(Path destDir, String entryName) throws IOException { + Path resolved = destDir.resolve(entryName).normalize(); + if (!resolved.startsWith(destDir)) { + throw new IOException("ZIP entry outside target dir: " + entryName); + } + return resolved; + } +} diff --git a/framework/extensions/src/main/resources/META-INF/cloudstack/core/spring-framework-extensions-core-context.xml b/framework/extensions/src/main/resources/META-INF/cloudstack/core/spring-framework-extensions-core-context.xml index 9d44d8ff7f3d..51f2df32dc69 100644 --- a/framework/extensions/src/main/resources/META-INF/cloudstack/core/spring-framework-extensions-core-context.xml +++ b/framework/extensions/src/main/resources/META-INF/cloudstack/core/spring-framework-extensions-core-context.xml @@ -31,6 +31,7 @@ + diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/util/YamlParserTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/util/YamlParserTest.java new file mode 100644 index 000000000000..e237fe5f9922 --- /dev/null +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/util/YamlParserTest.java @@ -0,0 +1,31 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 org.apache.cloudstack.framework.extensions.util; + +import org.junit.Test; + +import junit.framework.TestCase; + +public class YamlParserTest extends TestCase { + + @Test + public void testParseYaml() { + String yamlFilePath = getClass().getResource("manifest.yaml").getFile(); + YamlParser.parseYamlFile(yamlFilePath); + } +} diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/util/ZipExtractorTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/util/ZipExtractorTest.java new file mode 100644 index 000000000000..457d1e6140c1 --- /dev/null +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/util/ZipExtractorTest.java @@ -0,0 +1,17 @@ +package org.apache.cloudstack.framework.extensions.util; + +import java.io.IOException; + +import org.junit.Test; + +import junit.framework.TestCase; + +public class ZipExtractorTest extends TestCase { + + @Test + public void testExtractZip() throws IOException { + String zipFile = "/Users/manoj/Downloads/cloudstack-firecracker-extension-main.zip"; + ZipExtractor.extractZipContents(zipFile, "/tmp/firecracker"); + } + +} diff --git a/framework/extensions/src/test/resources/org/apache/cloudstack/framework/extensions/util/manifest.yaml b/framework/extensions/src/test/resources/org/apache/cloudstack/framework/extensions/util/manifest.yaml new file mode 100644 index 000000000000..4b151cdfcf59 --- /dev/null +++ b/framework/extensions/src/test/resources/org/apache/cloudstack/framework/extensions/util/manifest.yaml @@ -0,0 +1,95 @@ +apiVersion: cloudstack.apache.org/v1 +kind: OrchestratorExtension + +metadata: + name: firecracker + displayName: Firecracker Extension + description: > + External orchestrator extension that integrates Firecracker microVMs + with Apache CloudStack via the Orchestrator Extension Framework. + version: 0.1.0 + maintainer: "Marco Sinhoreli " + homepage: "https://github.com/msinhore/cloudstack-firecracker-extension" + +source: + type: git + url: "https://github.com/msinhore/cloudstack-firecracker-extension" + refs: "branchName" + +spec: + type: Orchestrator + + compatibility: + cloudstack: + minVersion: 4.23.0 + + entrypoint: + language: python + path: firecracker.py + targetDir: /usr/share/cloudstack-management/extensions/firecracker + + orchestrator: + requiresPrepareVm: false + + details: + key1: value1 + key2: value2 + + enabled: true + + customActions: + - name: CreateSnapshot + displayName: "Create Snapshot" + description: "Create a snapshot for a virtual machine instance." + resourcetype: VirtualMachine + enabled: true + timeout: 600 + allowedroletypes: [Admin, DomainAdmin, User] + parameters: + - name: snap_name + type: STRING + validationformat: NONE + required: true + - name: snap_description + type: STRING + validationformat: NONE + required: false + - name: snap_save_memory + type: BOOLEAN + validationformat: NONE + required: false + + - name: ListSnapshots + displayName: "List Snapshots" + description: "List all snapshots for a virtual machine instance." + resourcetype: VirtualMachine + enabled: true + timeout: 120 + allowedroletypes: [Admin, DomainAdmin, User] + parameters: [] + + - name: RestoreSnapshot + displayName: "Restore Snapshot" + description: "Restore a virtual machine instance from a given snapshot." + resourcetype: VirtualMachine + enabled: true + timeout: 900 + allowedroletypes: [Admin, DomainAdmin, User] + parameters: + - name: snap_name + type: STRING + validationformat: NONE + required: true + + - name: DeleteSnapshot + displayName: "Delete Snapshot" + description: "Delete a snapshot for a virtual machine instance." + resourcetype: VirtualMachine + enabled: true + timeout: 300 + allowedroletypes: [Admin, DomainAdmin, User] + parameters: + - name: snap_name + type: STRING + validationformat: NONE + required: true diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 529929eb5243..4cf2515ac03d 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -1246,6 +1246,7 @@ "label.images": "Images", "label.imagestoreid": "Secondary Storage", "label.import.backup.offering": "Import Backup Offering", +"label.import.extension": "Import Extension", "label.import.instance": "Import Instance", "label.import.offering": "Import Offering", "label.import.role": "Import Role", @@ -1517,6 +1518,7 @@ "label.management.servers": "Management Servers", "label.management.server.peers": "Peers", "label.managementservers": "Number of management servers", +"label.manifesturl": "Manifest URL", "label.matchall": "Match all", "label.max": "Max.", "label.max.primary.storage": "Max. primary (GiB)", diff --git a/ui/src/config/section/extension.js b/ui/src/config/section/extension.js index 4c6d9ebf0761..60f483a0da54 100644 --- a/ui/src/config/section/extension.js +++ b/ui/src/config/section/extension.js @@ -86,6 +86,15 @@ export default { popup: true, component: shallowRef(defineAsyncComponent(() => import('@/views/extension/CreateExtension.vue'))) }, + { + api: 'importExtension', + icon: 'cloud-upload-outlined', + label: 'label.import.extension', + docHelp: 'adminguide/extensions.html', + listView: true, + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/extension/ImportExtension.vue'))) + }, { api: 'updateExtension', icon: 'edit-outlined', diff --git a/ui/src/views/extension/ImportExtension.vue b/ui/src/views/extension/ImportExtension.vue new file mode 100644 index 000000000000..2e8fd55b6c15 --- /dev/null +++ b/ui/src/views/extension/ImportExtension.vue @@ -0,0 +1,116 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + + + + + + From bc2280425c68273b142e78c4d06f1916f4b12ff5 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 20 Nov 2025 04:33:23 +0530 Subject: [PATCH 2/5] fix Signed-off-by: Abhishek Kumar --- .../manager/ExtensionsImportManagerImpl.java | 16 ++++----- .../extensions/util/ExtensionConfig.java | 33 ++++++++++++------- .../extensions/util/YamlParserTest.java | 5 +-- .../extensions/util/ZipExtractorTest.java | 17 ---------- .../util/{manifest.yaml => testmanifest.yaml} | 17 +++++----- 5 files changed, 39 insertions(+), 49 deletions(-) delete mode 100644 framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/util/ZipExtractorTest.java rename framework/extensions/src/test/resources/org/apache/cloudstack/framework/extensions/util/{manifest.yaml => testmanifest.yaml} (80%) diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsImportManagerImpl.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsImportManagerImpl.java index f8ddcdd876f9..444f763d4ba8 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsImportManagerImpl.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsImportManagerImpl.java @@ -22,10 +22,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Collection; import java.util.Collections; -import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.UUID; @@ -41,6 +38,7 @@ import org.apache.commons.lang3.StringUtils; import com.cloud.hypervisor.ExternalProvisioner; +import com.cloud.utils.FileUtil; import com.cloud.utils.HttpUtils; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.db.Transaction; @@ -92,14 +90,12 @@ protected Extension importExtensionInternal(String manifestUrl, Path tempDir) { false, Collections.emptyMap()); for (ExtensionConfig.CustomAction action : extensionConfig.spec.customActions) { - List> parameters = action.getParametersMapList(); - Map>> parametersMap = new HashMap<>(); - parametersMap.put(1, parameters); + Map> parameters = action.getParametersAsMap(); extensionsManager.addCustomAction(action.name, action.description, extension.getId(), - action.resourcetype, action.allowedroletypes, action.timeout, true, parametersMap, + action.resourcetype, action.allowedroletypes, action.timeout, true, parameters, null, null, Collections.emptyMap()); } - return null; + return extension; }); } @@ -123,8 +119,8 @@ public Extension importExtension(ImportExtensionCmd cmd) { } catch (Exception e) { logger.error(e.getMessage(), e); throw e; - }/* finally { + } finally { FileUtil.deletePath(tempDir.toString()); - }*/ + } } } diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/util/ExtensionConfig.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/util/ExtensionConfig.java index 03e75e9346d2..04daff1ec917 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/util/ExtensionConfig.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/util/ExtensionConfig.java @@ -17,9 +17,11 @@ package org.apache.cloudstack.framework.extensions.util; +import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; + +import org.apache.cloudstack.api.ApiConstants; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -99,15 +101,18 @@ public static class CustomAction { public List allowedroletypes; public List parameters; - public List> getParametersMapList() { - return parameters.stream().map(param -> { - Map paramMap = new java.util.HashMap<>(); - paramMap.put("name", param.name); - paramMap.put("type", param.type); - paramMap.put("validationformat", param.validationformat); - paramMap.put("required", Boolean.toString(param.required)); - return paramMap; - }).collect(Collectors.toList()); + public Map> getParametersAsMap() { + Map> paramMap = new HashMap<>(); + int index = 0; + for (Parameter param : parameters) { + Map singleParamMap = new HashMap<>(); + singleParamMap.put(ApiConstants.NAME, param.name); + singleParamMap.put(ApiConstants.TYPE, param.type); + singleParamMap.put(ApiConstants.VALIDATION_FORMAT, param.validationformat); + singleParamMap.put(ApiConstants.REQUIRED, Boolean.toString(param.required)); + paramMap.put(index++, singleParamMap); + } + return paramMap; } } @@ -119,7 +124,13 @@ public static class Parameter { } public String getArchiveUrl() { - return source.url + "archive/refs/heads/" + source.refs + ".zip"; + String type = source != null ? source.type : null; + if ("git".equalsIgnoreCase(type) && source.url != null && source.url.contains("github.com")) { + // ToDo: improve + String ref = source.refs != null ? source.refs : "main"; + return source.url.replace("github.com", "codeload.github.com") + "/zip/refs/heads/" + ref; + } + return source == null ? null : source.url; } public Spec getSpec() { diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/util/YamlParserTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/util/YamlParserTest.java index e237fe5f9922..c45ad5778159 100644 --- a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/util/YamlParserTest.java +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/util/YamlParserTest.java @@ -25,7 +25,8 @@ public class YamlParserTest extends TestCase { @Test public void testParseYaml() { - String yamlFilePath = getClass().getResource("manifest.yaml").getFile(); - YamlParser.parseYamlFile(yamlFilePath); + String yamlFilePath = getClass().getResource("testmanifest.yaml").getFile(); + ExtensionConfig config = YamlParser.parseYamlFile(yamlFilePath); + assertNotNull(config); } } diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/util/ZipExtractorTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/util/ZipExtractorTest.java deleted file mode 100644 index 457d1e6140c1..000000000000 --- a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/util/ZipExtractorTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.apache.cloudstack.framework.extensions.util; - -import java.io.IOException; - -import org.junit.Test; - -import junit.framework.TestCase; - -public class ZipExtractorTest extends TestCase { - - @Test - public void testExtractZip() throws IOException { - String zipFile = "/Users/manoj/Downloads/cloudstack-firecracker-extension-main.zip"; - ZipExtractor.extractZipContents(zipFile, "/tmp/firecracker"); - } - -} diff --git a/framework/extensions/src/test/resources/org/apache/cloudstack/framework/extensions/util/manifest.yaml b/framework/extensions/src/test/resources/org/apache/cloudstack/framework/extensions/util/testmanifest.yaml similarity index 80% rename from framework/extensions/src/test/resources/org/apache/cloudstack/framework/extensions/util/manifest.yaml rename to framework/extensions/src/test/resources/org/apache/cloudstack/framework/extensions/util/testmanifest.yaml index 4b151cdfcf59..78663f9ded28 100644 --- a/framework/extensions/src/test/resources/org/apache/cloudstack/framework/extensions/util/manifest.yaml +++ b/framework/extensions/src/test/resources/org/apache/cloudstack/framework/extensions/util/testmanifest.yaml @@ -2,18 +2,17 @@ apiVersion: cloudstack.apache.org/v1 kind: OrchestratorExtension metadata: - name: firecracker - displayName: Firecracker Extension + name: test + displayName: Test Extension description: > - External orchestrator extension that integrates Firecracker microVMs - with Apache CloudStack via the Orchestrator Extension Framework. + Test extension via the Orchestrator Extension Framework. version: 0.1.0 - maintainer: "Marco Sinhoreli " - homepage: "https://github.com/msinhore/cloudstack-firecracker-extension" + maintainer: "Test Maintainer " + homepage: "https://github.com/maintainer/test" source: type: git - url: "https://github.com/msinhore/cloudstack-firecracker-extension" + url: "https://github.com/maintainer/test" refs: "branchName" spec: @@ -25,8 +24,8 @@ spec: entrypoint: language: python - path: firecracker.py - targetDir: /usr/share/cloudstack-management/extensions/firecracker + path: test.py + targetDir: /usr/share/cloudstack-management/extensions/test orchestrator: requiresPrepareVm: false From 57a7ffd2f90fb7b301eb6c3ed85277cbddcd1f69 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 20 Nov 2025 04:40:35 +0530 Subject: [PATCH 3/5] license Signed-off-by: Abhishek Kumar --- .../framework/extensions/util/testmanifest.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/framework/extensions/src/test/resources/org/apache/cloudstack/framework/extensions/util/testmanifest.yaml b/framework/extensions/src/test/resources/org/apache/cloudstack/framework/extensions/util/testmanifest.yaml index 78663f9ded28..da061e017f28 100644 --- a/framework/extensions/src/test/resources/org/apache/cloudstack/framework/extensions/util/testmanifest.yaml +++ b/framework/extensions/src/test/resources/org/apache/cloudstack/framework/extensions/util/testmanifest.yaml @@ -1,3 +1,20 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + apiVersion: cloudstack.apache.org/v1 kind: OrchestratorExtension From 22a8e5807748d3f145bcd4a47d69b3bad22ec28b Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 20 Nov 2025 04:46:52 +0530 Subject: [PATCH 4/5] more license Signed-off-by: Abhishek Kumar --- .../framework/extensions/util/YamlParser.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/util/YamlParser.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/util/YamlParser.java index 8d226d1fc00f..83da965ef6f4 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/util/YamlParser.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/util/YamlParser.java @@ -1,5 +1,21 @@ -package org.apache.cloudstack.framework.extensions.util; +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 org.apache.cloudstack.framework.extensions.util; import java.io.InputStream; import java.nio.file.Files; From 6cc3988f0501f7eaae46278276ec5b4e10bf7ef4 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Fri, 28 Nov 2025 15:52:36 +0530 Subject: [PATCH 5/5] Apply suggestion from @shwstppr --- .../cloudstack/framework/extensions/util/testmanifest.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/framework/extensions/src/test/resources/org/apache/cloudstack/framework/extensions/util/testmanifest.yaml b/framework/extensions/src/test/resources/org/apache/cloudstack/framework/extensions/util/testmanifest.yaml index da061e017f28..9495da6b58f8 100644 --- a/framework/extensions/src/test/resources/org/apache/cloudstack/framework/extensions/util/testmanifest.yaml +++ b/framework/extensions/src/test/resources/org/apache/cloudstack/framework/extensions/util/testmanifest.yaml @@ -40,7 +40,6 @@ spec: minVersion: 4.23.0 entrypoint: - language: python path: test.py targetDir: /usr/share/cloudstack-management/extensions/test