diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 00000000..26e8bc1a --- /dev/null +++ b/Changelog.md @@ -0,0 +1,11 @@ +# Change Log +This file contains all the notable changes done to the Ballerina Cloud package through the releases. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [3.0.0] - Unreleased + +### Fixed + +- [Fix the inconsistency in overwriting identical cookies](https://github.com/ballerina-platform/ballerina-library/issues/6194) \ No newline at end of file diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/c2c/test/MixedConfigJobTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/c2c/test/MixedConfigJobTest.java index 257f456e..5d6109f8 100644 --- a/compiler-plugin-tests/src/test/java/io/ballerina/c2c/test/MixedConfigJobTest.java +++ b/compiler-plugin-tests/src/test/java/io/ballerina/c2c/test/MixedConfigJobTest.java @@ -121,7 +121,7 @@ public void validateDeployment() { // Validate config file Assert.assertEquals(container.getEnv().get(0).getName(), "BAL_CONFIG_FILES"); Assert.assertEquals(container.getEnv().get(0).getValue(), - "/home/ballerina/conf1/Config1.toml:/home/ballerina/conf/Config.toml:" + + "/home/ballerina/conf/Config.toml:/home/ballerina/conf1/Config1.toml:" + "/home/ballerina/secrets/mysql-secrets.toml:/home/ballerina/secrets1/additional-secrets.toml:"); } diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/c2c/test/MixedConfigTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/c2c/test/MixedConfigTest.java index d978164e..52b17de1 100644 --- a/compiler-plugin-tests/src/test/java/io/ballerina/c2c/test/MixedConfigTest.java +++ b/compiler-plugin-tests/src/test/java/io/ballerina/c2c/test/MixedConfigTest.java @@ -130,7 +130,7 @@ public void validateDeployment() { // Validate config file Assert.assertEquals(container.getEnv().get(0).getName(), "BAL_CONFIG_FILES"); Assert.assertEquals(container.getEnv().get(0).getValue(), - "/home/ballerina/conf1/Config1.toml:/home/ballerina/conf/Config.toml:" + + "/home/ballerina/conf/Config.toml:/home/ballerina/conf1/Config1.toml:" + "/home/ballerina/secrets/mysql-secrets.toml:/home/ballerina/secrets1/additional-secrets.toml:"); } diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/c2c/test/MultipleConfigTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/c2c/test/MultipleConfigTest.java index 0f63a0f6..7947a8e2 100644 --- a/compiler-plugin-tests/src/test/java/io/ballerina/c2c/test/MultipleConfigTest.java +++ b/compiler-plugin-tests/src/test/java/io/ballerina/c2c/test/MultipleConfigTest.java @@ -78,7 +78,7 @@ public void compileSample() throws IOException, InterruptedException { case "config-config-map": ballerinaConf = (ConfigMap) data; break; - case "hello-data-txt": + case "hello-data-txtcfg0": dataMap = (ConfigMap) data; break; default: diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/c2c/test/samples/Sample5Test.java b/compiler-plugin-tests/src/test/java/io/ballerina/c2c/test/samples/Sample5Test.java index 52841f6c..0ebaba3b 100644 --- a/compiler-plugin-tests/src/test/java/io/ballerina/c2c/test/samples/Sample5Test.java +++ b/compiler-plugin-tests/src/test/java/io/ballerina/c2c/test/samples/Sample5Test.java @@ -111,14 +111,14 @@ public void validateDeployment() { Assert.assertNotNull(deployment); Assert.assertEquals(deployment.getMetadata().getName(), "hello-deployment"); Assert.assertEquals(deployment.getSpec().getReplicas().intValue(), 1); - Assert.assertEquals(deployment.getSpec().getTemplate().getSpec().getVolumes().size(), 5); + Assert.assertEquals(deployment.getSpec().getTemplate().getSpec().getVolumes().size(), 4); Assert.assertEquals(deployment.getMetadata().getLabels().get(KubernetesConstants .KUBERNETES_SELECTOR_KEY), "hello"); Assert.assertEquals(deployment.getSpec().getTemplate().getSpec().getContainers().size(), 1); // Assert Containers Container container = deployment.getSpec().getTemplate().getSpec().getContainers().get(0); - Assert.assertEquals(container.getVolumeMounts().size(), 5); + Assert.assertEquals(container.getVolumeMounts().size(), 4); Assert.assertEquals(container.getImage(), DOCKER_IMAGE); Assert.assertEquals(container.getPorts().size(), 1); Assert.assertEquals(container.getEnv().size(), 1); diff --git a/compiler-plugin-tests/src/test/resources/command/create-cloud-toml/config/create_cloud_toml_cmd.json b/compiler-plugin-tests/src/test/resources/command/create-cloud-toml/config/create_cloud_toml_cmd.json index d1e5d6e7..7f8df5ec 100644 --- a/compiler-plugin-tests/src/test/resources/command/create-cloud-toml/config/create_cloud_toml_cmd.json +++ b/compiler-plugin-tests/src/test/resources/command/create-cloud-toml/config/create_cloud_toml_cmd.json @@ -23,7 +23,7 @@ "character": 0 } }, - "newText": "# This file contains most used configurations supported by Ballerina Code to Cloud\n# All the fields are optional. If these fields are not specified, default value will be taken from the compiler.\n# Full Code to Cloud specification can be accessed from https://github.com/ballerina-platform/ballerina-spec/blob/master/c2c/code-to-cloud-spec.md\n# Uncomment Any field below if you want to override the default value.\n\n# Settings related to artifacts generation\n#[settings]\n#buildImage = true # Build the Docker image while building the project\n#thinJar = true # Use the thin jars in the container\n#\n# Properties related to the container image\n#[container.image]\n#name = \"hello\" # Name of the container image\n#repository = \"ballerina\" # Container repository to host the container\n#tag = \"latest\" # Tag of the container\n#base = \"ballerina/jvm-runtime:2.0\" # Base container of the container image\n#\n# Copy the files to the container image\n#[[container.copy.files]]\n#sourceFile = \"./data/data.txt\" # Path to the external file\n#target = \"/home/ballerina/data/data.txt\" # Path of the file within the container\n#\n# External files required for the code\n#[[cloud.config.maps]]\n#file = \"resource/file.text\" # Path of the external file\n#mount_path = \"/home/ballerina/foo/file.conf\" # Path of the file in the container\n#\n# Environment variables required for the application\n#[[cloud.config.envs]]\n#key_ref = \"FOO\" # Key of the environment variable\n#name = \"foo\" # Name of the env if it is different from the key\n#config_name = \"module-foo\" # Name of the config config map\n#\n# Properties related to the deployment\n#[cloud.deployment]\n#min_memory = \"100Mi\" # Minimum memory allocated to the container\n#max_memory = \"512Mi\" # Maximum memory allocated to the container\n#min_cpu = \"200m\" # Minimum CPU allocated to the container\n#max_cpu = \"500m\" # Maximum CPU allocated to the container\n#\n# Matrices to auto-scale the container\n#[cloud.deployment.autoscaling]\n#min_replicas = 1 # Minimum number of replicas of the container alive at a given time\n#max_replicas = 2 # Maximum number of replicas of the container alive at a given time\n#cpu = 50 # CPU Utilization threshold for spawning a new instance\n#\n# Probe to indicate whether the container is ready to respond to requests. No readiness probe will be generated if not specified\n#[cloud.deployment.probes.readiness]\n#port = 9091 # Port of the readiness probe endpoint\n#path = \"/probes/readyz\" # Endpoint of the readiness probe\n#\n# Probe to indicate whether the container is running. No liveness probe will be generated if not specified\n#[cloud.deployment.probes.liveness]\n#port = 9091 # Port of the liveness probe endpoint\n#path = \"/probes/healthz\" # Endpoint of the liveness probe\n#\n# Volume definitions of the application. No default volumes will be generated if not specified\n#[[cloud.deployment.storage.volumes]]\n#name = \"volume1\" # Name of the volume\n#local_path = \"files\" # Path of the volume\n#size = \"2Gi\" # Maximum size of the volume\n#\n# Properties related to the builder image of the multistage build\n#[graalvm.builder]\n#base = \"ghcr.io/graalvm/native-image-community:17-ol8\" # base image of the builder image\n#buildCmd = \"native-image -jar hello.jar hello\" # RUN statement to build the native image\n#" + "newText": "# This file contains most used configurations supported by Ballerina Code to Cloud\n# All the fields are optional. If these fields are not specified, default value will be taken from the compiler.\n# Full Code to Cloud specification can be accessed from https://github.com/ballerina-platform/ballerina-spec/blob/master/c2c/code-to-cloud-spec.md\n# Uncomment Any field below if you want to override the default value.\n\n# Settings related to artifacts generation\n#[settings]\n#buildImage = true # Build the Docker image while building the project\n#thinJar = true # Use the thin jars in the container\n#\n# Properties related to the container image\n#[container.image]\n#name = \"hello\" # Name of the container image\n#repository = \"ballerina\" # Container repository to host the container\n#tag = \"latest\" # Tag of the container\n#base = \"ballerina/jvm-runtime:2.0\" # Base container of the container image\n#\n# Copy the files to the container image\n#[[container.copy.files]]\n#sourceFile = \"./data/data.txt\" # Path to the external file\n#target = \"/home/ballerina/data/data.txt\" # Path of the file within the container\n#\n# External files required for the code\n#[[cloud.config.maps]]\n#file = \"resource/file.txt\" # Path of the external file\n#mount_dir = \"/home/ballerina/resource\" # Directory of the file within the container\n#\n# Environment variables required for the application\n#[[cloud.config.envs]]\n#key_ref = \"FOO\" # Key of the environment variable\n#name = \"foo\" # Name of the env if it is different from the key\n#config_name = \"module-foo\" # Name of the config config map\n#\n# Properties related to the deployment\n#[cloud.deployment]\n#min_memory = \"100Mi\" # Minimum memory allocated to the container\n#max_memory = \"512Mi\" # Maximum memory allocated to the container\n#min_cpu = \"200m\" # Minimum CPU allocated to the container\n#max_cpu = \"500m\" # Maximum CPU allocated to the container\n#\n# Matrices to auto-scale the container\n#[cloud.deployment.autoscaling]\n#min_replicas = 1 # Minimum number of replicas of the container alive at a given time\n#max_replicas = 2 # Maximum number of replicas of the container alive at a given time\n#cpu = 50 # CPU Utilization threshold for spawning a new instance\n#\n# Probe to indicate whether the container is ready to respond to requests. No readiness probe will be generated if not specified\n#[cloud.deployment.probes.readiness]\n#port = 9091 # Port of the readiness probe endpoint\n#path = \"/probes/readyz\" # Endpoint of the readiness probe\n#\n# Probe to indicate whether the container is running. No liveness probe will be generated if not specified\n#[cloud.deployment.probes.liveness]\n#port = 9091 # Port of the liveness probe endpoint\n#path = \"/probes/healthz\" # Endpoint of the liveness probe\n#\n# Volume definitions of the application. No default volumes will be generated if not specified\n#[[cloud.deployment.storage.volumes]]\n#name = \"volume1\" # Name of the volume\n#local_path = \"files\" # Path of the volume\n#size = \"2Gi\" # Maximum size of the volume\n#\n# Properties related to the builder image of the multistage build\n#[graalvm.builder]\n#base = \"ghcr.io/graalvm/native-image-community:17-ol8\" # base image of the builder image\n#buildCmd = \"native-image -jar hello.jar hello\" # RUN statement to build the native image\n#" } ] } diff --git a/compiler-plugin-tests/src/test/resources/completion/cloud/main/config/config4.json b/compiler-plugin-tests/src/test/resources/completion/cloud/main/config/config4.json index fc78d85f..cc5da7ce 100644 --- a/compiler-plugin-tests/src/test/resources/completion/cloud/main/config/config4.json +++ b/compiler-plugin-tests/src/test/resources/completion/cloud/main/config/config4.json @@ -20,11 +20,11 @@ "insertText": "[settings]" }, { - "label": "mount_path", + "label": "mount_dir", "kind": "Snippet", "detail": "String", "sortText": "A", - "insertText": "mount_path=\"${1:}\"", + "insertText": "mount_dir=\"${1:}\"", "insertTextFormat": "Snippet" }, { diff --git a/compiler-plugin-tests/src/test/resources/multiple-config/Cloud.toml b/compiler-plugin-tests/src/test/resources/multiple-config/Cloud.toml index 82922339..4742a833 100644 --- a/compiler-plugin-tests/src/test/resources/multiple-config/Cloud.toml +++ b/compiler-plugin-tests/src/test/resources/multiple-config/Cloud.toml @@ -5,7 +5,7 @@ tag="v1" [[cloud.config.maps]] file="./conf/data.txt" - mount_path="/home/ballerina/data" + mount_dir="/home/ballerina/data" [[cloud.config.files]] file="./conf/Config1.toml" [[cloud.config.files]] @@ -13,8 +13,8 @@ tag="v1" [[cloud.secret.files]] file="resource/public.crt" -mount_path="/home/ballerina/resource/public.crt" +mount_dir="/home/ballerina/resource" [[cloud.secret.files]] file="resource/private.key" -mount_path="/home/ballerina/resource/private.key" \ No newline at end of file +mount_dir="/home/ballerina/resource" \ No newline at end of file diff --git a/compiler-plugin-tests/src/test/resources/service/grpc-test/Cloud.toml b/compiler-plugin-tests/src/test/resources/service/grpc-test/Cloud.toml index 44c4772b..be67eeb3 100644 --- a/compiler-plugin-tests/src/test/resources/service/grpc-test/Cloud.toml +++ b/compiler-plugin-tests/src/test/resources/service/grpc-test/Cloud.toml @@ -1,10 +1,10 @@ [[cloud.secret.files]] file="resources/public.crt" -mount_path="/home/ballerina/resources/public.crt" +mount_dir="/home/ballerina/resources" [[cloud.secret.files]] file="resources/public.crt" -mount_path="/home/ballerina/resources/public.crt" +mount_dir="/home/ballerina/resources" [settings] buildImage = false diff --git a/compiler-plugin-tests/src/test/resources/settings/multi-yaml/Cloud.toml b/compiler-plugin-tests/src/test/resources/settings/multi-yaml/Cloud.toml index 111da089..de64905d 100644 --- a/compiler-plugin-tests/src/test/resources/settings/multi-yaml/Cloud.toml +++ b/compiler-plugin-tests/src/test/resources/settings/multi-yaml/Cloud.toml @@ -4,17 +4,13 @@ tag="v1" [[cloud.config.maps]] file="./conf/data.txt" - mount_path="/home/ballerina/data" + mount_dir="/home/ballerina/data" [[cloud.config.files]] file="./conf/Config.toml" [[cloud.secret.files]] -file="resource/private.key" -mount_path="/home/ballerina/resource/private.key" - -[[cloud.secret.files]] -file="resource/public.crt" -mount_path="/home/ballerina/resource/public.crt" +file="./resource" +mount_dir="/home/ballerina/resource" [settings] singleYAML=false diff --git a/compiler-plugin/src/main/java/io/ballerina/c2c/CloudTomlResolver.java b/compiler-plugin/src/main/java/io/ballerina/c2c/CloudTomlResolver.java index 1ab892f2..5dbec1f9 100644 --- a/compiler-plugin/src/main/java/io/ballerina/c2c/CloudTomlResolver.java +++ b/compiler-plugin/src/main/java/io/ballerina/c2c/CloudTomlResolver.java @@ -42,6 +42,7 @@ import io.fabric8.kubernetes.api.model.Quantity; import org.apache.commons.codec.binary.Base64; +import java.io.File; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; @@ -216,6 +217,7 @@ private void resolveResourcesToml(DeploymentModel deploymentModel, Toml deployme } private void resolveConfigSecretToml(Toml toml) throws KubernetesPluginException { + //TODO change spec List secrets = toml.getTables("cloud.config.secrets"); for (int i = 0, secretsSize = secrets.size(); i < secretsSize; i++) { Toml secret = secrets.get(i); @@ -270,6 +272,7 @@ private void resolveConfigMapToml(Toml toml) throws KubernetesPluginException { configMapModel.setData(dataMap); configMapModel.setBallerinaConf(true); configMapModel.setReadOnly(false); + configMapModel.setDir(false); dataHolder.addConfigMaps(Collections.singleton(configMapModel)); } else { ConfigMapModel existingConfigMap = configMap.get(); @@ -300,18 +303,43 @@ private void resolveSecretToml(KubernetesModel kubernetesModel, Toml toml) throw for (int i = 0, secretsSize = secrets.size(); i < secretsSize; i++) { Toml secret = secrets.get(i); Path path = Paths.get(Objects.requireNonNull(TomlHelper.getString(secret, "file"))); - Path mountPath = Paths.get(Objects.requireNonNull(TomlHelper.getString(secret, "mount_path", + Path mountPath = Paths.get(Objects.requireNonNull(TomlHelper.getString(secret, "mount_dir", getSecretMountPath(i)))); final Path fileName = validatePaths(path, mountPath); + File file = path.toFile(); + if (!file.exists()) { + Diagnostic diagnostic = C2CDiagnosticCodes.createDiagnostic( + C2CDiagnosticCodes.PATH_CONTENT_READ_FAILED, new NullLocation(), path.toString()); + throw new KubernetesPluginException(diagnostic); + } SecretModel secretModel = new SecretModel(); - secretModel.setName(deploymentName + "-" + getValidName(fileName.toString())); + if (!mountPath.isAbsolute()) { + mountPath = Paths.get(BALLERINA_HOME, mountPath.toString()); + } + + String mountPathSr = mountPath.toString(); + if (file.isDirectory()) { + secretModel.setDir(true); + } else { + mountPathSr = getModifiedMountPath(mountPath.toString(), fileName.toString()); + } + + secretModel.setName(deploymentName + "-" + getValidName(fileName.toString()) + SECRET_POSTFIX + i); secretModel.setData(getDataForSecret(path.toString())); - secretModel.setMountPath(mountPath.toString()); + secretModel.setMountPath(mountPathSr); dataHolder.addSecrets(Collections.singleton(secretModel)); } } } + private String getModifiedMountPath(String mountDir, String fileName) { + if (mountDir.endsWith("/")) { + return mountDir + fileName; + } else { + return mountDir + "/" + fileName; + } + } + private Optional getConfigMapModel(String name) { Set configMapModelSet = dataHolder.getConfigMapModelSet(); for (ConfigMapModel configMapModel : configMapModelSet) { @@ -326,14 +354,30 @@ public void resolveConfigFilesToml(KubernetesModel kubernetesModel, Toml toml) t List configFiles = toml.getTables("cloud.config.maps"); if (configFiles.size() != 0) { final String deploymentName = kubernetesModel.getName().replace(DEPLOYMENT_POSTFIX, ""); - for (Toml configFile : configFiles) { + for (int i = 0; i < configFiles.size(); i++) { + Toml configFile = configFiles.get(i); Path path = Paths.get(Objects.requireNonNull(TomlHelper.getString(configFile, "file"))); - Path mountPath = Paths.get(Objects.requireNonNull(TomlHelper.getString(configFile, "mount_path"))); + Path mountPath = Paths.get(Objects.requireNonNull(TomlHelper.getString(configFile, "mount_dir"))); final Path fileName = validatePaths(path, mountPath); ConfigMapModel configMapModel = new ConfigMapModel(); - configMapModel.setName(deploymentName + "-" + getValidName(fileName.toString())); + if (!mountPath.isAbsolute()) { + mountPath = Paths.get(BALLERINA_HOME, mountPath.toString()); + } + File file = path.toFile(); + if (!file.exists()) { + Diagnostic diagnostic = C2CDiagnosticCodes.createDiagnostic( + C2CDiagnosticCodes.PATH_CONTENT_READ_FAILED, new NullLocation(), path.toString()); + throw new KubernetesPluginException(diagnostic); + } + String mountPathSr = mountPath.toString(); + if (file.isDirectory()) { + configMapModel.setDir(true); + } else { + mountPathSr = getModifiedMountPath(mountPath.toString(), fileName.toString()); + } + configMapModel.setName(deploymentName + "-" + getValidName(fileName.toString()) + "cfg" + i); configMapModel.setData(getDataForConfigMap(path.toString())); - configMapModel.setMountPath(mountPath.toString()); + configMapModel.setMountPath(mountPathSr); configMapModel.setBallerinaConf(false); dataHolder.addConfigMaps(Collections.singleton(configMapModel)); } @@ -380,9 +424,25 @@ private Map getDataForConfigMap(String path) throws KubernetesPl if (!dataFilePath.isAbsolute()) { dataFilePath = KubernetesContext.getInstance().getDataHolder().getSourceRoot().resolve(dataFilePath); } - String key = String.valueOf(dataFilePath.getFileName()); - String content = new String(KubernetesUtils.readFileContent(dataFilePath), StandardCharsets.UTF_8); - dataMap.put(key, content); + File file = dataFilePath.toFile(); + if (!file.isDirectory()) { + String key = String.valueOf(dataFilePath.getFileName()); + String content = new String(KubernetesUtils.readFileContent(dataFilePath), StandardCharsets.UTF_8); + dataMap.put(key, content); + return dataMap; + } + File[] files = file.listFiles(); + if (files == null) { + return dataMap; + } + for (File f : files) { + if (f.isDirectory()) { + continue; + } + String key = f.getName(); + String content = new String(KubernetesUtils.readFileContent(dataFilePath), StandardCharsets.UTF_8); + dataMap.put(key, content); + } return dataMap; } @@ -392,9 +452,26 @@ private Map getDataForSecret(String path) throws KubernetesPlugi if (!dataFilePath.isAbsolute()) { dataFilePath = KubernetesContext.getInstance().getDataHolder().getSourceRoot().resolve(dataFilePath); } - String key = String.valueOf(dataFilePath.getFileName()); - String content = Base64.encodeBase64String(KubernetesUtils.readFileContent(dataFilePath)); - dataMap.put(key, content); + File file = dataFilePath.toFile(); + if (!file.isDirectory()) { + // Read all files + String key = String.valueOf(dataFilePath.getFileName()); + String content = Base64.encodeBase64String(KubernetesUtils.readFileContent(dataFilePath)); + dataMap.put(key, content); + return dataMap; + } + File[] files = file.listFiles(); + if (files == null) { + return dataMap; + } + for (File f : files) { + if (f.isDirectory()) { + continue; + } + String key = f.getName(); + String content = Base64.encodeBase64String(KubernetesUtils.readFileContent(f.toPath())); + dataMap.put(key, content); + } return dataMap; } diff --git a/compiler-plugin/src/main/java/io/ballerina/c2c/KubernetesConstants.java b/compiler-plugin/src/main/java/io/ballerina/c2c/KubernetesConstants.java index 3530ba08..0ab77b94 100644 --- a/compiler-plugin/src/main/java/io/ballerina/c2c/KubernetesConstants.java +++ b/compiler-plugin/src/main/java/io/ballerina/c2c/KubernetesConstants.java @@ -47,8 +47,8 @@ public class KubernetesConstants { public static final String DOCKER_LATEST_TAG = ":latest"; public static final String BALLERINA_HOME = "/home/ballerina"; public static final String BALLERINA_RUNTIME = "/ballerina/runtime"; - public static final String BALLERINA_CONF_MOUNT_PATH = "/home/ballerina/conf"; - public static final String BALLERINA_CONF_SECRETS_MOUNT_PATH = "/home/ballerina/secrets"; + public static final String BALLERINA_CONF_MOUNT_PATH = BALLERINA_HOME + "/conf"; + public static final String BALLERINA_CONF_SECRETS_MOUNT_PATH = BALLERINA_HOME + "/secrets"; public static final String BALLERINA_CONF_FILE_NAME = "Config.toml"; public static final String DOCKER_HOST = "DOCKER_HOST"; public static final String DOCKER_CERT_PATH = "DOCKER_CERT_PATH"; diff --git a/compiler-plugin/src/main/java/io/ballerina/c2c/handlers/DeploymentHandler.java b/compiler-plugin/src/main/java/io/ballerina/c2c/handlers/DeploymentHandler.java index 4afcbbc5..8c9e5093 100644 --- a/compiler-plugin/src/main/java/io/ballerina/c2c/handlers/DeploymentHandler.java +++ b/compiler-plugin/src/main/java/io/ballerina/c2c/handlers/DeploymentHandler.java @@ -44,8 +44,6 @@ import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; @@ -61,31 +59,27 @@ public class DeploymentHandler extends AbstractArtifactHandler { private List populateVolumeMounts(DeploymentModel deploymentModel) { List volumeMounts = new ArrayList<>(); for (SecretModel secretModel : deploymentModel.getSecretModels()) { - VolumeMount volumeMount = new VolumeMountBuilder() + VolumeMountBuilder volumeMountBuilder = new VolumeMountBuilder() .withMountPath(secretModel.getMountPath()) .withName(secretModel.getName() + "-volume") - .withReadOnly(secretModel.isReadOnly()) - .build(); + .withReadOnly(secretModel.isReadOnly()); + if ((!secretModel.isDir()) && (!secretModel.isBallerinaConf())) { + volumeMountBuilder.withSubPath(KubernetesUtils.getFileNameOfSecret(secretModel)); + } + VolumeMount volumeMount = volumeMountBuilder.build(); volumeMounts.add(volumeMount); } for (ConfigMapModel configMapModel : deploymentModel.getConfigMapModels()) { final String mountPath = configMapModel.getMountPath(); - if (mountPath != null) { - VolumeMount volumeMount = new VolumeMountBuilder() - .withMountPath(mountPath) - .withName(configMapModel.getName() + "-volume") - .withReadOnly(configMapModel.isReadOnly()) - .build(); + VolumeMountBuilder volumeMountBuilder = new VolumeMountBuilder() + .withMountPath(mountPath) + .withName(configMapModel.getName() + "-volume") + .withReadOnly(configMapModel.isReadOnly()); - if (KubernetesUtils.getExtension(mountPath).isPresent()) { - // Add file mount as sub paths. - final Path fileName = Paths.get(mountPath).getFileName(); - if (null != fileName) { - volumeMount.setSubPath(fileName.toString()); - } - } - volumeMounts.add(volumeMount); + if ((!configMapModel.isDir()) && (!configMapModel.isBallerinaConf())) { + volumeMountBuilder.withSubPath(KubernetesUtils.getFileNameOfConfigMap(configMapModel)); } + volumeMounts.add(volumeMountBuilder.build()); } for (PersistentVolumeClaimModel volumeClaimModel : deploymentModel.getVolumeClaimModels()) { VolumeMount volumeMount = new VolumeMountBuilder() diff --git a/compiler-plugin/src/main/java/io/ballerina/c2c/handlers/JobHandler.java b/compiler-plugin/src/main/java/io/ballerina/c2c/handlers/JobHandler.java index 8c8113cb..eccc2b9a 100644 --- a/compiler-plugin/src/main/java/io/ballerina/c2c/handlers/JobHandler.java +++ b/compiler-plugin/src/main/java/io/ballerina/c2c/handlers/JobHandler.java @@ -42,8 +42,6 @@ import io.fabric8.kubernetes.api.model.batch.v1.JobBuilder; import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; @@ -183,31 +181,28 @@ private DockerModel getDockerModel(JobModel jobModel) throws DockerGenException private List populateVolumeMounts() { List volumeMounts = new ArrayList<>(); for (SecretModel secretModel : dataHolder.getSecretModelSet()) { - VolumeMount volumeMount = new VolumeMountBuilder() + VolumeMountBuilder volumeMountBuilder = new VolumeMountBuilder() .withMountPath(secretModel.getMountPath()) .withName(secretModel.getName() + "-volume") - .withReadOnly(secretModel.isReadOnly()) - .build(); - volumeMounts.add(volumeMount); + .withReadOnly(secretModel.isReadOnly()); + + if ((!secretModel.isDir()) && (!secretModel.isBallerinaConf())) { + volumeMountBuilder.withSubPath(KubernetesUtils.getFileNameOfSecret(secretModel)); + } + + volumeMounts.add(volumeMountBuilder.build()); } for (ConfigMapModel configMapModel : dataHolder.getConfigMapModelSet()) { final String mountPath = configMapModel.getMountPath(); - if (mountPath != null) { - VolumeMount volumeMount = new VolumeMountBuilder() - .withMountPath(mountPath) - .withName(configMapModel.getName() + "-volume") - .withReadOnly(configMapModel.isReadOnly()) - .build(); - - if (KubernetesUtils.getExtension(mountPath).isPresent()) { - // Add file mount as sub paths. - final Path fileName = Paths.get(mountPath).getFileName(); - if (null != fileName) { - volumeMount.setSubPath(fileName.toString()); - } - } - volumeMounts.add(volumeMount); + VolumeMountBuilder volumeMountBuilder = new VolumeMountBuilder() + .withMountPath(mountPath) + .withName(configMapModel.getName() + "-volume") + .withReadOnly(configMapModel.isReadOnly()); + + if ((!configMapModel.isDir()) && (!configMapModel.isBallerinaConf())) { + volumeMountBuilder.withSubPath(KubernetesUtils.getFileNameOfConfigMap(configMapModel)); } + volumeMounts.add(volumeMountBuilder.build()); } return volumeMounts; } diff --git a/compiler-plugin/src/main/java/io/ballerina/c2c/models/ConfigMapModel.java b/compiler-plugin/src/main/java/io/ballerina/c2c/models/ConfigMapModel.java index 5cad5b2c..6675718f 100644 --- a/compiler-plugin/src/main/java/io/ballerina/c2c/models/ConfigMapModel.java +++ b/compiler-plugin/src/main/java/io/ballerina/c2c/models/ConfigMapModel.java @@ -33,8 +33,10 @@ public class ConfigMapModel extends KubernetesModel { private boolean readOnly; private boolean isBallerinaConf; private int defaultMode; + private boolean dir; public ConfigMapModel() { this.readOnly = true; + this.dir = false; } } diff --git a/compiler-plugin/src/main/java/io/ballerina/c2c/models/KubernetesDataHolder.java b/compiler-plugin/src/main/java/io/ballerina/c2c/models/KubernetesDataHolder.java index a0820d14..e2f6f6d6 100644 --- a/compiler-plugin/src/main/java/io/ballerina/c2c/models/KubernetesDataHolder.java +++ b/compiler-plugin/src/main/java/io/ballerina/c2c/models/KubernetesDataHolder.java @@ -24,10 +24,8 @@ import java.nio.file.Path; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; /** @@ -39,7 +37,6 @@ public class KubernetesDataHolder { private DockerModel dockerModel; private PodAutoscalerModel podAutoscalerModel; private List serviceModelList; - private Map> bListenerToSecretMap; private Set secretModelSet; private Set configMapModelSet; private JobModel jobModel; @@ -56,7 +53,6 @@ public class KubernetesDataHolder { KubernetesDataHolder() { this.serviceModelList = new ArrayList<>(); - this.bListenerToSecretMap = new HashMap<>(); this.secretModelSet = new HashSet<>(); this.configMapModelSet = new HashSet<>(); this.deploymentModel = new DeploymentModel(); @@ -65,10 +61,6 @@ public class KubernetesDataHolder { this.singleYaml = true; } - public void addListenerSecret(String listenerName, Set secretModel) { - this.bListenerToSecretMap.put(listenerName, secretModel); - } - public void addSecrets(Set secrets) { this.secretModelSet.addAll(secrets); } diff --git a/compiler-plugin/src/main/java/io/ballerina/c2c/models/SecretModel.java b/compiler-plugin/src/main/java/io/ballerina/c2c/models/SecretModel.java index 6e4c1673..82415df3 100644 --- a/compiler-plugin/src/main/java/io/ballerina/c2c/models/SecretModel.java +++ b/compiler-plugin/src/main/java/io/ballerina/c2c/models/SecretModel.java @@ -33,8 +33,10 @@ public class SecretModel extends KubernetesModel { private boolean readOnly; private boolean isBallerinaConf; private int defaultMode; + private boolean dir; public SecretModel() { this.readOnly = true; + this.dir = false; } } diff --git a/compiler-plugin/src/main/java/io/ballerina/c2c/utils/DockerGenerator.java b/compiler-plugin/src/main/java/io/ballerina/c2c/utils/DockerGenerator.java index b0a299ee..11e82ff3 100644 --- a/compiler-plugin/src/main/java/io/ballerina/c2c/utils/DockerGenerator.java +++ b/compiler-plugin/src/main/java/io/ballerina/c2c/utils/DockerGenerator.java @@ -19,6 +19,7 @@ package io.ballerina.c2c.utils; import io.ballerina.c2c.DockerGenConstants; +import io.ballerina.c2c.KubernetesConstants; import io.ballerina.c2c.exceptions.DockerGenException; import io.ballerina.c2c.models.CopyFileModel; import io.ballerina.c2c.models.DockerModel; @@ -265,6 +266,6 @@ protected void appendCommonCommands(StringBuilder dockerfileContent) { } private String getWorkDir() { - return "/home/ballerina"; + return KubernetesConstants.BALLERINA_HOME; } } diff --git a/compiler-plugin/src/main/java/io/ballerina/c2c/utils/KubernetesUtils.java b/compiler-plugin/src/main/java/io/ballerina/c2c/utils/KubernetesUtils.java index 24c8d722..05562935 100644 --- a/compiler-plugin/src/main/java/io/ballerina/c2c/utils/KubernetesUtils.java +++ b/compiler-plugin/src/main/java/io/ballerina/c2c/utils/KubernetesUtils.java @@ -26,6 +26,7 @@ import io.ballerina.c2c.diagnostics.NullLocation; import io.ballerina.c2c.exceptions.DockerGenException; import io.ballerina.c2c.exceptions.KubernetesPluginException; +import io.ballerina.c2c.models.ConfigMapModel; import io.ballerina.c2c.models.CopyFileModel; import io.ballerina.c2c.models.DeploymentModel; import io.ballerina.c2c.models.DockerModel; @@ -33,6 +34,7 @@ import io.ballerina.c2c.models.KubernetesContext; import io.ballerina.c2c.models.KubernetesDataHolder; import io.ballerina.c2c.models.KubernetesModel; +import io.ballerina.c2c.models.SecretModel; import io.ballerina.c2c.util.C2CDiagnosticCodes; import io.ballerina.projects.Package; import io.ballerina.toml.api.Toml; @@ -54,6 +56,7 @@ import java.util.Comparator; import java.util.HashSet; import java.util.Locale; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -246,6 +249,16 @@ public static Optional getExtension(String filename) { .map(f -> f.substring(filename.lastIndexOf(".") + 1)); } + public static String getFileNameOfSecret(SecretModel secretModel) { + Map data = secretModel.getData(); + return data.keySet().iterator().next(); + } + + public static String getFileNameOfConfigMap(ConfigMapModel configMapModel) { + Map data = configMapModel.getData(); + return data.keySet().iterator().next(); + } + public static void resolveDockerToml(KubernetesModel model) throws KubernetesPluginException { KubernetesDataHolder dataHolder = KubernetesContext.getInstance().getDataHolder(); final String containerImage = "container.image"; diff --git a/compiler-plugin/src/main/java/io/ballerina/c2c/utils/NativeDockerGenerator.java b/compiler-plugin/src/main/java/io/ballerina/c2c/utils/NativeDockerGenerator.java index ee418335..47421f66 100644 --- a/compiler-plugin/src/main/java/io/ballerina/c2c/utils/NativeDockerGenerator.java +++ b/compiler-plugin/src/main/java/io/ballerina/c2c/utils/NativeDockerGenerator.java @@ -17,6 +17,7 @@ */ package io.ballerina.c2c.utils; +import io.ballerina.c2c.KubernetesConstants; import io.ballerina.c2c.exceptions.DockerGenException; import io.ballerina.c2c.models.CopyFileModel; import io.ballerina.c2c.models.DockerModel; @@ -111,6 +112,6 @@ private String generateMultiStageDockerfile() { @Override protected void appendUser(StringBuilder dockerfileContent) { - dockerfileContent.append("WORKDIR ").append("/home/ballerina").append(LINE_SEPARATOR); + dockerfileContent.append("WORKDIR ").append(KubernetesConstants.BALLERINA_HOME).append(LINE_SEPARATOR); } } diff --git a/compiler-plugin/src/main/resources/c2c-schema.json b/compiler-plugin/src/main/resources/c2c-schema.json index 82fdc9b7..453d0b73 100644 --- a/compiler-plugin/src/main/resources/c2c-schema.json +++ b/compiler-plugin/src/main/resources/c2c-schema.json @@ -155,18 +155,18 @@ "description": "Path of the external file", "type": "string", "pattern": "^(?!\\s*$).+", - "default": "resource/file.text", + "default": "resource/file.txt", "message": { "pattern": "`file` should not be empty" } }, - "mount_path": { - "description": "Path of the file in the container", + "mount_dir": { + "description": "Directory of the file within the container", "type": "string", "pattern": "^(?!\\s*$).+", - "default": "/home/ballerina/foo/file.conf", + "default": "/home/ballerina/resource", "message": { - "pattern": "`mount_path` should not be empty" + "pattern": "`mount_dir` should not be empty" } } } @@ -451,12 +451,12 @@ "pattern": "`file` should not be empty" } }, - "mount_path": { - "description": "Path of the file within the container", + "mount_dir": { + "description": "Directory of the file within the container", "type": "string", "pattern": "^(?!\\s*$).+", "message": { - "pattern": "`mount_path` should not be empty" + "pattern": "`mount_dir` should not be empty" } } } diff --git a/examples/kubernetes-mount-config-map-volumes/Cloud.toml b/examples/kubernetes-mount-config-map-volumes/Cloud.toml index cd570048..48ef3ce8 100644 --- a/examples/kubernetes-mount-config-map-volumes/Cloud.toml +++ b/examples/kubernetes-mount-config-map-volumes/Cloud.toml @@ -4,11 +4,11 @@ name="hello-api" # Name of the container image tag="sample5" # Tag of the container [[cloud.config.maps]] -file="./conf/data.txt" # Path of the external file -mount_path="/home/ballerina/data" # Path of the file in the container +file="./data/data.txt" # Path of the external file +mount_dir="./data" # Dir of the file in the container [[cloud.config.files]] -file="./conf/Config.toml" # Path of the external file +file="./data/Config.toml" # Path of the external file name="sample5-config-map" [[cloud.config.secrets]] @@ -16,9 +16,5 @@ file="./mysql-secrets.toml" name="mysql-secrets" [[cloud.secret.files]] -file="resource/public.crt" -mount_path="/home/ballerina/resource/public.crt" - -[[cloud.secret.files]] -file="resource/private.key" -mount_path="/home/ballerina/resource/private.key" \ No newline at end of file +file="./resource" +mount_dir="./resource" diff --git a/examples/kubernetes-mount-config-map-volumes/README.md b/examples/kubernetes-mount-config-map-volumes/README.md index 763e6ad5..90d94124 100644 --- a/examples/kubernetes-mount-config-map-volumes/README.md +++ b/examples/kubernetes-mount-config-map-volumes/README.md @@ -10,106 +10,57 @@ This segment shows how a c2c segment is mapped into cloud element. 1. `Cloud.toml` segment ```toml [[cloud.config.maps]] -file="./conf/data.txt" # Path of the external file -mount_path="/home/ballerina/data" # Path of the file in the container +file="./data/data.txt" # Path of the external file +mount_dir="./data" # Dir of the file in the container [[cloud.config.files]] -file="./conf/Config.toml" # Path of the external file -name="sample5-config-map" +file="./data/Config.toml" # Path of the external file +name="sample5-config-map" + +[[cloud.config.secrets]] +file="./mysql-secrets.toml" +name="mysql-secrets" + +[[cloud.secret.files]] +file="./resource" +mount_dir="./resource" + ``` 2. Kubernetes YAML file segment ```yaml +--- apiVersion: "v1" kind: "ConfigMap" metadata: name: "sample5-config-map" data: - Config.toml: "[hello.hello]\nusers = \"john@ballerina.com,jane@ballerina.com\"\n\ + Config.toml: "[xlight.hello]\nusers = \"john@ballerina.com,jane@ballerina.com\"\n\ groups = \"apim,esb\"\n" --- apiVersion: "v1" kind: "ConfigMap" metadata: - name: "hello-data-txt" + name: "hello-data-txtcfg0" data: data.txt: "Lorem ipsum dolor sit amet." -``` -A kubernetes `ConfigMap` componenet is generated with the file content. -```yaml -apiVersion: "apps/v1" -kind: "Deployment" +--- +apiVersion: "v1" +kind: "Secret" metadata: - . - . - . -spec: - . - . - . - template: - . - . - . - spec: - containers: - - env: - - name: "BAL_CONFIG_FILES" - value: "/home/ballerina/conf/Config.toml:" - . - . - . - volumeMounts: - . - . - . - - mountPath: "/home/ballerina/conf/" - name: "sample5-config-map-volume" - readOnly: false - - mountPath: "/home/ballerina/data" - name: "hello-data-txt-volume" - readOnly: true - nodeSelector: {} - volumes: - . - . - . - - configMap: - name: "sample5-config-map" - name: "sample5-config-map-volume" - - configMap: - name: "hello-data-txt" - name: "hello-data-txt-volume" -``` - Configmap `volumes` are created and are mounted as `volumeMounts` in the container. A additional step is carried out for the `[[cloud.config.files]]` table element which is used to mount configurations. These configuartions are added to `-env` section of YAML file as `BAL_CONFIG_FILES`. - -1. ```Ballerina file``` segment -```bal -listener http:Listener helloWorldEP = new(9090, { - secureSocket: { - key: { - certFile: "./resource/public.crt", - keyFile: "./resource/private.key" - } - } -}); - -``` - Above `secureSocket`,`mutualSsl` components can be mapped to the Kubernetes `Secret` componenet without explicitly being mentioned in `Cloud.toml`. This behavoiur is applicable to other SSL related stuffs. - -2. Kubernetes YAML file segment -```yaml + name: "mysql-secrets" +data: + mysql-secrets.toml: "W215c3FsXQpob3N0ID0gImxvY2FsaG9zdCIKdXNlciA9ICJ1c2VyIgo=" +--- apiVersion: "v1" kind: "Secret" metadata: - name: "helloworldep-secure-socket" + name: "hello-resource-secret0" data: - public.crt: "LS0tLS1CRUdJ....." - private.key: "LS0tLS1C........" - + public.crt: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURkekNDQWwrZ0F3SUJBZ0lFZlAzZTh6QU5CZ2txaGtpRzl3MEJBUXNGQURCa01Rc3dDUVlEVlFRR0V3SlYKVXpFTE1Ba0dBMVVFQ0JNQ1EwRXhGakFVQmdOVkJBY1REVTF2ZFc1MFlXbHVJRlpwWlhjeERUQUxCZ05WQkFvVApCRmRUVHpJeERUQUxCZ05WQkFzVEJGZFRUekl4RWpBUUJnTlZCQU1UQ1d4dlkyRnNhRzl6ZERBZUZ3MHhOekV3Ck1qUXdOVFEzTlRoYUZ3MHpOekV3TVRrd05UUTNOVGhhTUdReEN6QUpCZ05WQkFZVEFsVlRNUXN3Q1FZRFZRUUkKRXdKRFFURVdNQlFHQTFVRUJ4TU5UVzkxYm5SaGFXNGdWbWxsZHpFTk1Bc0dBMVVFQ2hNRVYxTlBNakVOTUFzRwpBMVVFQ3hNRVYxTlBNakVTTUJBR0ExVUVBeE1KYkc5allXeG9iM04wTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGCkFBT0NBUThBTUlJQkNnS0NBUUVBZ1Z5aTZmVmlWTGlaS0VudzU5eHpOaTFsY1loNno5ZFpudWcrRjlnS3FGSWcKbWRjUGUrcXRTN2daYzFqWVRqV01DYngxM3NGTGtacU5IZURVYWRwbXRLbzNURGR1T2wxc3FNNmN6M3lYYjZMMwo0ay9sZWg1MG16SVBObWFhWHhkM3ZPUW9LNE9wa2dPMW4zMm1oNit0a3Azc2JIbWZZcURRcmtWSzF0bVlOdFBKCmZmU0NMVCtDdUlobkpVSmNvN04wdW5heCt5U1pONjcvQVgrK3NKcHFBaEFJWkp6clJpNnVlTjNSRkNJeFlEWFMKTXZ4ckVtT2RuNGdPQzBvMUFyOXU1QnA5TjUyc3FxR2JOMXg2ak5LaTNiZlVqMTIySHU1ZStZOUtPbWZiY2hoUQppbDJQODFjSWkzMFZLZ3lEbjVEZVdFdURvWXJlZGs0KzZxQVpyeE13K3dJREFRQUJvekV3THpBT0JnTlZIUThCCkFmOEVCQU1DQmFBd0hRWURWUjBPQkJZRUZObXRyUTM2ajZ0VUdoS3JmVzlxV1dFN0tGek1NQTBHQ1NxR1NJYjMKRFFFQkN3VUFBNElCQVFBdjN5T3dnYnRPdTc2ZUpNbDFCQ2NnVEZnYU1VQlpvVWpLOVVuNkhHaktFZ1l6L1lXUwpaRmxZL3FINXJUMDFEV1FldlVaQjYyNmQ1Wk5kelNCWlJscHN4YmY5SUUvdXJzTkh3SHg5dWE2ZkI3eUhVQ3pDCjFaTXAxbHZCSEFCaTd3Y0ErNW5iVjZ6UTdIRG1CWEZoSmZiZ0gxaVZtQTFLY3ZEZUJQU0ovc2NSR2FzWjVxMlcKM0llbkROcmZQSVVoRDc0dEZpQ2lxTkpPOTFxRC9MTysrKzNYZVp6ZlBoOE5SS2tpUFg3ZEI4V0ozWU5CdVFBdgpnUldUSVNwU1NYTG1xTWIrN01QUVZnZWNzZXBaZGs4Q3drUkx4aDNSS1BKTWppZ21DZ3l2a1Nhb0RNS0FZQzNpCllqZlVUaUo1N1VlcW9TbDBJYU9GSjB3ZlpSRmgrVXl0bERaYQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==" + private.key: "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQ0JYS0xwOVdKVXVKa28KU2ZEbjNITTJMV1Z4aUhyUDExbWU2RDRYMkFxb1VpQ1oxdzk3NnExTHVCbHpXTmhPTll3SnZIWGV3VXVSbW8wZAo0TlJwMm1hMHFqZE1OMjQ2WFd5b3pwelBmSmR2b3ZmaVQrVjZIblNiTWc4MlpwcGZGM2U4NUNncmc2bVNBN1dmCmZhYUhyNjJTbmV4c2VaOWlvTkN1UlVyVzJaZzIwOGw5OUlJdFA0SzRpR2NsUWx5anMzUzZkckg3SkprM3J2OEIKZjc2d21tb0NFQWhrbk90R0xxNTQzZEVVSWpGZ05kSXkvR3NTWTUyZmlBNExTalVDdjI3a0duMDNuYXlxb1pzMwpYSHFNMHFMZHQ5U1BYYlllN2w3NWowbzZaOXR5R0ZDS1hZL3pWd2lMZlJVcURJT2ZrTjVZUzRPaGl0NTJUajdxCm9CbXZFekQ3QWdNQkFBRUNnZ0VBWE0vRjR1MjNPdW1tbVExVDFrYUlNcHFuYWFsdDA2akNHQXl3WUJNVXNtY2EKRk1ZRHlmZzVsVlhraktsMXA4Y3JUZUQxQUhqV2F3VGpza2dZbmttZjNvY3hYWEYzbUZCbklVWDdvN0hVUkxnNworUmN4b1Vnd2lSaUZhWlo3c3pYM0pvTGJmenpiY0hOUTM3a2F2Y2NCVld3UXNGTWlVM1RsdytMYkt3SzYvcm93CkxZc1FQeDdnVDR1N2hWaWF0NHZRRFRZY2d5anZ2RkNpZWs0bmRMNk85SzQ5TXhJTVU2NzhVWEI2aWE1aVVldnkKdmdFZmNZa0tRNUVRMzhxUzNad3N1YlB2ajQ2MzNqdkFKUnIvaEpEOFhJTlpDNzRrVFhlVjNCR0gyTGxwUU9FcQprV2tPeXB3WU5qblh0dDFKTzgrSXU2bUVYS1VvaUlCUGZHckozdkRTUVFLQmdRRG1ZUGM3a2ZZYW4vTEhqSlJ2CmlFMkN3YkMyNnlWQTYrQkVQUXY5ejdqQ2hPOVE2Y1ViR3ZNOEVFVk5wQzlubUZvZ2tzbHpKaHo1NUhQODRRWkwKdTNwdFUrRDk2bmNxNnprQnF4QmZSblpHKytEMzYrWFJYSXd6ejNoK2cxTndybDB5ME1GYndsa01tM1pxSmRkNgpwWnoxRlpHZDZ6dlFmdFc4bTdqUFNLSHVzd0tCZ1FDUHY2Y3pGT1pSNmJJK3FDUWRhT1JwZTlKR29BZHVPRCs0CllLbDk2czBlaUFLaGtHaEZDck1kNkdKd1dSa3BOY2Z3QitKOXNNYWhPUmJmdndpWWFuSTU2aDdWaTMwREZQUmIKbTFtOGRMa3I2eis4YnhNeEtKYU1YSUlqeTNVRGFtZ0RyN1FISW5OVWloMmlHdnRCOFFxWjBhb2JzQjJYSXhaZwpxRVNUTWNwWW1RS0JnSFN3U3FuZXJhUWd2Z3o3RkxoRmR0VXpIRG9hY3IwbWZHcXo3UjM3Rjk5WERBeVV5K1NGCnl3dnlSZGdrd0dvZGpoRVBxSC90bnlHbjZHUCs2bnh6a25oTDB4dHBwa0NUOGtUNUM0cm1tc1Fya25DaENMLzUKdTM0R3FVYVRhREViOEZMcnovU1ZSUnVRcHZMdkJleTJkQURqa3VWRkgvL2tMb2lnNjRQNml5TG5Bb0dCQUlsRgpnKzJMNzhZWlhWWG9TMVNxYmpVdFFVaWdXWGd2enVuTHBRL1J3YjkrTXNVR21nd1VnNmZ6MnMxZXlHQktNM3hNCmkwVnNJc0tqT2V6QkNQeEQ2b0RUeWs0eXZsYkxFKzdIRTVLY0JKaWtObUZEMFJnSW9udTNlNitqQTBNWHdleUQKUlcvcXZpZmxIUmRJbk5nRHp4UEUzS1ZFTVgyNnpBdlJwR3JNQ1dkQkFvR0FkUTVTdlgrbUFDM2NLcW9ROVphbApsU3FXb3lqZnpQNUVhVlJHOGR0b0x4YnpuUUdUVHZ0SFhjNjUvTXpuWC9MOXFrV0NTNkViNEhINU0zaEZOWTQ2CkxOSXpHUUx6bkUxb2R3djdINUI4YzAvbTNEcktUeGJoOGJZY3JSMUJXNS9uS1pOTlc3azFPNk9qRW96dkFhaksKSlFkcDNLQlU5UzhDbUJqR3JScEoycXc9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K" +--- ``` - -A kubernetes `Secret` component is created with the content of SSL stuffs. - +A kubernetes `ConfigMap` componenet is generated with the file content. ```yaml apiVersion: "apps/v1" kind: "Deployment" @@ -127,28 +78,41 @@ spec: . spec: containers: - . - . - . - volumeMounts: - - mountPath: "/home/ballerina/./resource" - name: "helloworldep-secure-socket-volume" - readOnly: true + - env: + - name: "BAL_CONFIG_FILES" + value: "/home/ballerina/conf/Config.toml:/home/ballerina/secrets/mysql-secrets.toml:" . . . - nodeSelector: {} + volumeMounts: + - mountPath: "/home/ballerina/secrets/" + name: "mysql-secrets-volume" + readOnly: true + - mountPath: "/home/ballerina/./resource" + name: "hello-resource-secret0-volume" + readOnly: true + - mountPath: "/home/ballerina/conf/" + name: "sample5-config-map-volume" + readOnly: false + - mountPath: "/home/ballerina/./data/data.txt" + name: "hello-data-txtcfg0-volume" + readOnly: true + subPath: "data.txt" volumes: - - name: "helloworldep-secure-socket-volume" - secret: - secretName: "helloworldep-secure-socket" - . - . - . - + - name: "mysql-secrets-volume" + secret: + secretName: "mysql-secrets" + - name: "hello-resource-secret0-volume" + secret: + secretName: "hello-resource-secret0" + - configMap: + name: "sample5-config-map" + name: "sample5-config-map-volume" + - configMap: + name: "hello-data-txtcfg0" + name: "hello-data-txtcfg0-volume" ``` - - `volume` and `volumeMount` are used to mount the secret to the container. + Configmap `volumes` are created and are mounted as `volumeMounts` in the container. A additional step is carried out for the `[[cloud.config.files]]` table element which is used to mount configurations. These configuartions are added to `-env` section of YAML file as `BAL_CONFIG_FILES`. ### How to run: diff --git a/examples/kubernetes-mount-config-map-volumes/conf/Config.toml b/examples/kubernetes-mount-config-map-volumes/data/Config.toml similarity index 100% rename from examples/kubernetes-mount-config-map-volumes/conf/Config.toml rename to examples/kubernetes-mount-config-map-volumes/data/Config.toml diff --git a/examples/kubernetes-mount-config-map-volumes/conf/data.txt b/examples/kubernetes-mount-config-map-volumes/data/data.txt similarity index 100% rename from examples/kubernetes-mount-config-map-volumes/conf/data.txt rename to examples/kubernetes-mount-config-map-volumes/data/data.txt diff --git a/examples/kubernetes-multi-module-ballerina-project/Cloud.toml b/examples/kubernetes-multi-module-ballerina-project/Cloud.toml index abe05dbf..b444fce6 100644 --- a/examples/kubernetes-multi-module-ballerina-project/Cloud.toml +++ b/examples/kubernetes-multi-module-ballerina-project/Cloud.toml @@ -6,8 +6,8 @@ tag="1.0.0" # Tag of the container [[cloud.config.maps]] file="./menus/tea.json" # Path of the external file - mount_path="/home/ballerina/menus/tea.json" # Path of the file in the container + mount_dir="/home/ballerina/menus" # Path of the file in the container [[cloud.config.maps]] file="./menus/coffe.json" # Path of the external file - mount_path="/home/ballerina/menus/coffe.json" # Path of the file in the container + mount_dir="/home/ballerina/menus" # Path of the file in the container diff --git a/examples/kubernetes-multi-module-ballerina-project/README.md b/examples/kubernetes-multi-module-ballerina-project/README.md index cf3408e0..8f74bf68 100644 --- a/examples/kubernetes-multi-module-ballerina-project/README.md +++ b/examples/kubernetes-multi-module-ballerina-project/README.md @@ -10,11 +10,12 @@ This segment shows how a c2c segment is mapped into cloud element. ```toml [[cloud.config.maps]] file="./menus/tea.json" # Path of the external file -mount_path="/home/ballerina/menus/tea.json" # Path of the file in the container +mount_dir="/home/ballerina/menus" # Dir of the file in the container [[cloud.config.maps]] file="./menus/coffe.json" # Path of the external file -mount_path="/home/ballerina/menus/coffe.json" # Path of the file in the container +mount_dir="/home/ballerina/menus" # Dir of the file in the container + ``` 2. Kubernetes YAML file segment @@ -65,22 +66,21 @@ spec: . . volumeMounts: - - mountPath: "/home/ballerina/menus/tea.json" - name: "cafe-tea-json-volume" - readOnly: true - subPath: "tea.json" - - mountPath: "/home/ballerina/menus/coffe.json" - name: "cafe-coffe-json-volume" - readOnly: true - subPath: "coffe.json" - nodeSelector: {} + - mountPath: "/home/ballerina/menus/tea.json" + name: "cafe-tea-jsoncfg0-volume" + readOnly: true + subPath: "tea.json" + - mountPath: "/home/ballerina/menus/coffe.json" + name: "cafe-coffe-jsoncfg1-volume" + readOnly: true + subPath: "coffe.json" volumes: - - configMap: - name: "cafe-tea-json" - name: "cafe-tea-json-volume" - - configMap: - name: "cafe-coffe-json" - name: "cafe-coffe-json-volume" + - configMap: + name: "cafe-tea-jsoncfg0" + name: "cafe-tea-jsoncfg0-volume" + - configMap: + name: "cafe-coffe-jsoncfg1" + name: "cafe-coffe-jsoncfg1-volume" ``` ConfigMap `volumes` are created and are mounted as `volumeMounts` in the container. diff --git a/examples/kubernetes-secret-mounts/Cloud.toml b/examples/kubernetes-secret-mounts/Cloud.toml index 9fcd0572..5bf6869b 100644 --- a/examples/kubernetes-secret-mounts/Cloud.toml +++ b/examples/kubernetes-secret-mounts/Cloud.toml @@ -4,8 +4,8 @@ name="hello-api" # Name of the container image tag="sample12" # Tag of the container [[cloud.secret.files]] -file="./conf/data.txt" # External file path to mount as a secret volume -mount_path="/home/ballerina/data" # Path of the file within the container +file="./data/data.txt" # Path to the secret file +mount_dir="./data" # Path of the file within the container -[[cloud.secret.files]] -file="./conf/Config.toml" # External file path to mount as a secret volume +[[cloud.config.secrets]] +file="./data/Config.toml" # Path to the secret Config.toml diff --git a/examples/kubernetes-secret-mounts/README.md b/examples/kubernetes-secret-mounts/README.md index 67e2a93c..320b917c 100644 --- a/examples/kubernetes-secret-mounts/README.md +++ b/examples/kubernetes-secret-mounts/README.md @@ -7,11 +7,11 @@ This segment shows how a c2c segment is mapped into cloud element. 1. ```Cloud.toml``` segment ```toml [[cloud.secret.files]] -file="./conf/data.txt" # External file path to mount as a secret volume -mount_path="/home/ballerina/data" # Path of the file within the container +file="./data/data.txt" # Path to the secret file +mount_dir="./data" # Path of the file within the container -[[cloud.secret.files]] -file="./conf/Config.toml" # External file path to mount as a secret volume +[[cloud.config.secrets]] +file="./data/Config.toml" # Path to the secret Config.toml ``` 2. Kubernetes YAML file segment @@ -19,18 +19,17 @@ file="./conf/Config.toml" # External file path to mount as a secret volume apiVersion: "v1" kind: "Secret" metadata: - name: "hello-data-txt" + name: "config-secret" data: - data.txt: "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQu" + Config.toml: "W2hlbGxvLmhlbGxvXQpncmVldGluZyA9ICJoZWxsbyIK" --- apiVersion: "v1" kind: "Secret" metadata: - name: "hello-ballerina-conf-secret" + name: "hello-data-txt-secret0" data: - Config.toml: "W2hlbGxvLmhlbGxvXQp1c2VycyA9ICJqb2huQGJhbGxlcmluYS5jb20samFuZUBiYWxsZXJpbmEuY29tIgpncm91cHMgPSAiYXBpbSxlc2IiCg==" - - + data.txt: "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQu" +--- ``` A kubernetes `Secret` component is created with the content of files. @@ -54,21 +53,21 @@ spec: . . . - volumeMounts: - - mountPath: "/home/ballerina/data" - name: "hello-data-txt-volume" - readOnly: true - - mountPath: "/home/ballerina/conf/" - name: "hello-ballerina-conf-secret-volume" - readOnly: false - nodeSelector: {} + volumeMounts: + - mountPath: "/home/ballerina/secrets/" + name: "config-secret-volume" + readOnly: true + - mountPath: "/home/ballerina/./data/data.txt" + name: "hello-data-txt-secret0-volume" + readOnly: true + subPath: "data.txt" volumes: - - name: "hello-data-txt-volume" - secret: - secretName: "hello-data-txt" - - name: "hello-ballerina-conf-secret-volume" - secret: - secretName: "hello-ballerina-conf-secret" + - name: "config-secret-volume" + secret: + secretName: "config-secret" + - name: "hello-data-txt-secret0-volume" + secret: + secretName: "hello-data-txt-secret0" ``` `volume` and `volumeMount` are used to mount the secret to the container. diff --git a/examples/kubernetes-secret-mounts/conf/Config.toml b/examples/kubernetes-secret-mounts/conf/Config.toml deleted file mode 100644 index 0b9079a8..00000000 --- a/examples/kubernetes-secret-mounts/conf/Config.toml +++ /dev/null @@ -1,3 +0,0 @@ -[hello.hello] -users = "john@ballerina.com,jane@ballerina.com" -groups = "apim,esb" diff --git a/examples/kubernetes-secret-mounts/data/Config.toml b/examples/kubernetes-secret-mounts/data/Config.toml new file mode 100644 index 00000000..e7f85ab7 --- /dev/null +++ b/examples/kubernetes-secret-mounts/data/Config.toml @@ -0,0 +1,2 @@ +[hello.hello] +greeting = "hello" diff --git a/examples/kubernetes-secret-mounts/conf/data.txt b/examples/kubernetes-secret-mounts/data/data.txt similarity index 100% rename from examples/kubernetes-secret-mounts/conf/data.txt rename to examples/kubernetes-secret-mounts/data/data.txt diff --git a/examples/kubernetes-secret-mounts/hello_secret.bal b/examples/kubernetes-secret-mounts/hello_secret.bal index e9365253..e95c6a31 100644 --- a/examples/kubernetes-secret-mounts/hello_secret.bal +++ b/examples/kubernetes-secret-mounts/hello_secret.bal @@ -1,37 +1,15 @@ import ballerina/io; import ballerina/http; -import ballerina/log; -listener http:Listener helloWorldEP = new(9090, { - secureSocket: { - key: { - certFile: "./resource1/public.crt", - keyFile: "./resource/private.key" - } - } -}); +configurable string greeting = "meow"; + +listener http:Listener helloWorldEP = new(9090); service /helloWorld on helloWorldEP { - resource function get data(http:Caller caller, http:Request request) { - http:Response response = new; - string payload = <@untainted> readFile("./data/data.txt"); - response.setTextPayload("Data: " + <@untainted> payload + "\n"); - var responseResult = caller->respond(response); - if (responseResult is error) { - log:printError("error responding back to client.", 'error = responseResult); - } + resource function get data() returns string|error { + io:println(greeting); + return io:fileReadString("./data/data.txt"); } } -function readFile(string filePath) returns @tainted string { - io:ReadableByteChannel bchannel = checkpanic io:openReadableFile(filePath); - io:ReadableCharacterChannel cChannel = new io:ReadableCharacterChannel(bchannel, "UTF-8"); - - var readOutput = cChannel.read(50); - if (readOutput is string) { - return readOutput; - } else { - return "Error: Unable to read file"; - } -} diff --git a/examples/kubernetes-secret-mounts/resource/private.key b/examples/kubernetes-secret-mounts/resource/private.key deleted file mode 100644 index fcdc5dc6..00000000 --- a/examples/kubernetes-secret-mounts/resource/private.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCBXKLp9WJUuJko -SfDn3HM2LWVxiHrP11me6D4X2AqoUiCZ1w976q1LuBlzWNhONYwJvHXewUuRmo0d -4NRp2ma0qjdMN246XWyozpzPfJdvovfiT+V6HnSbMg82ZppfF3e85Cgrg6mSA7Wf -faaHr62SnexseZ9ioNCuRUrW2Zg208l99IItP4K4iGclQlyjs3S6drH7JJk3rv8B -f76wmmoCEAhknOtGLq543dEUIjFgNdIy/GsSY52fiA4LSjUCv27kGn03nayqoZs3 -XHqM0qLdt9SPXbYe7l75j0o6Z9tyGFCKXY/zVwiLfRUqDIOfkN5YS4Ohit52Tj7q -oBmvEzD7AgMBAAECggEAXM/F4u23OummmQ1T1kaIMpqnaalt06jCGAywYBMUsmca -FMYDyfg5lVXkjKl1p8crTeD1AHjWawTjskgYnkmf3ocxXXF3mFBnIUX7o7HURLg7 -+RcxoUgwiRiFaZZ7szX3JoLbfzzbcHNQ37kavccBVWwQsFMiU3Tlw+LbKwK6/row -LYsQPx7gT4u7hViat4vQDTYcgyjvvFCiek4ndL6O9K49MxIMU678UXB6ia5iUevy -vgEfcYkKQ5EQ38qS3ZwsubPvj4633jvAJRr/hJD8XINZC74kTXeV3BGH2LlpQOEq -kWkOypwYNjnXtt1JO8+Iu6mEXKUoiIBPfGrJ3vDSQQKBgQDmYPc7kfYan/LHjJRv -iE2CwbC26yVA6+BEPQv9z7jChO9Q6cUbGvM8EEVNpC9nmFogkslzJhz55HP84QZL -u3ptU+D96ncq6zkBqxBfRnZG++D36+XRXIwzz3h+g1Nwrl0y0MFbwlkMm3ZqJdd6 -pZz1FZGd6zvQftW8m7jPSKHuswKBgQCPv6czFOZR6bI+qCQdaORpe9JGoAduOD+4 -YKl96s0eiAKhkGhFCrMd6GJwWRkpNcfwB+J9sMahORbfvwiYanI56h7Vi30DFPRb -m1m8dLkr6z+8bxMxKJaMXIIjy3UDamgDr7QHInNUih2iGvtB8QqZ0aobsB2XIxZg -qESTMcpYmQKBgHSwSqneraQgvgz7FLhFdtUzHDoacr0mfGqz7R37F99XDAyUy+SF -ywvyRdgkwGodjhEPqH/tnyGn6GP+6nxzknhL0xtppkCT8kT5C4rmmsQrknChCL/5 -u34GqUaTaDEb8FLrz/SVRRuQpvLvBey2dADjkuVFH//kLoig64P6iyLnAoGBAIlF -g+2L78YZXVXoS1SqbjUtQUigWXgvzunLpQ/Rwb9+MsUGmgwUg6fz2s1eyGBKM3xM -i0VsIsKjOezBCPxD6oDTyk4yvlbLE+7HE5KcBJikNmFD0RgIonu3e6+jA0MXweyD -RW/qviflHRdInNgDzxPE3KVEMX26zAvRpGrMCWdBAoGAdQ5SvX+mAC3cKqoQ9Zal -lSqWoyjfzP5EaVRG8dtoLxbznQGTTvtHXc65/MznX/L9qkWCS6Eb4HH5M3hFNY46 -LNIzGQLznE1odwv7H5B8c0/m3DrKTxbh8bYcrR1BW5/nKZNNW7k1O6OjEozvAajK -JQdp3KBU9S8CmBjGrRpJ2qw= ------END PRIVATE KEY----- diff --git a/examples/kubernetes-secret-mounts/resource1/public.crt b/examples/kubernetes-secret-mounts/resource1/public.crt deleted file mode 100644 index 77f91743..00000000 --- a/examples/kubernetes-secret-mounts/resource1/public.crt +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIEfP3e8zANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJV -UzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxDTALBgNVBAoT -BFdTTzIxDTALBgNVBAsTBFdTTzIxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xNzEw -MjQwNTQ3NThaFw0zNzEwMTkwNTQ3NThaMGQxCzAJBgNVBAYTAlVTMQswCQYDVQQI -EwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzENMAsGA1UEChMEV1NPMjENMAsG -A1UECxMEV1NPMjESMBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAgVyi6fViVLiZKEnw59xzNi1lcYh6z9dZnug+F9gKqFIg -mdcPe+qtS7gZc1jYTjWMCbx13sFLkZqNHeDUadpmtKo3TDduOl1sqM6cz3yXb6L3 -4k/leh50mzIPNmaaXxd3vOQoK4OpkgO1n32mh6+tkp3sbHmfYqDQrkVK1tmYNtPJ -ffSCLT+CuIhnJUJco7N0unax+ySZN67/AX++sJpqAhAIZJzrRi6ueN3RFCIxYDXS -MvxrEmOdn4gOC0o1Ar9u5Bp9N52sqqGbN1x6jNKi3bfUj122Hu5e+Y9KOmfbchhQ -il2P81cIi30VKgyDn5DeWEuDoYredk4+6qAZrxMw+wIDAQABozEwLzAOBgNVHQ8B -Af8EBAMCBaAwHQYDVR0OBBYEFNmtrQ36j6tUGhKrfW9qWWE7KFzMMA0GCSqGSIb3 -DQEBCwUAA4IBAQAv3yOwgbtOu76eJMl1BCcgTFgaMUBZoUjK9Un6HGjKEgYz/YWS -ZFlY/qH5rT01DWQevUZB626d5ZNdzSBZRlpsxbf9IE/ursNHwHx9ua6fB7yHUCzC -1ZMp1lvBHABi7wcA+5nbV6zQ7HDmBXFhJfbgH1iVmA1KcvDeBPSJ/scRGasZ5q2W -3IenDNrfPIUhD74tFiCiqNJO91qD/LO+++3XeZzfPh8NRKkiPX7dB8WJ3YNBuQAv -gRWTISpSSXLmqMb+7MPQVgecsepZdk8CwkRLxh3RKPJMjigmCgyvkSaoDMKAYC3i -YjfUTiJ57UeqoSl0IaOFJ0wfZRFh+UytlDZa ------END CERTIFICATE----- diff --git a/examples/kubernetes-secrets-keystore-truststore/Cloud.toml b/examples/kubernetes-secrets-keystore-truststore/Cloud.toml index 3dee2183..5a5e28db 100644 --- a/examples/kubernetes-secrets-keystore-truststore/Cloud.toml +++ b/examples/kubernetes-secrets-keystore-truststore/Cloud.toml @@ -5,8 +5,8 @@ tag="sample9" # Tag of the container [[cloud.secret.files]] file="resource/ballerinaKeystore.p12" -mount_path="/home/ballerina/resource/ballerinaKeystore.p12" +mount_dir="/home/ballerina/resource" [[cloud.secret.files]] file="resource1/ballerinaKeystore.p12" -mount_path="/home/ballerina/resource1/ballerinaKeystore.p12" +mount_dir="/home/ballerina/resource1" diff --git a/examples/kubernetes-secrets-keystore-truststore/README.md b/examples/kubernetes-secrets-keystore-truststore/README.md index c9f307b9..43707eab 100644 --- a/examples/kubernetes-secrets-keystore-truststore/README.md +++ b/examples/kubernetes-secrets-keystore-truststore/README.md @@ -23,9 +23,19 @@ listener http:Listener helloWorldEP = new(9090, { }); ``` - Above `secureSocket`,`mutualSsl` components can be mapped to the Kubernetes `Secret` componenet without explicitly being mentioned in `Cloud.toml`. This behavoiur is applicable to other SSL related stuffs. - -2. Kubernetes YAML file segment + +2. Cloud.toml +```toml +[[cloud.secret.files]] +file="resource/ballerinaKeystore.p12" +mount_dir="/home/ballerina/resource + +[[cloud.secret.files]] +file="resource1/ballerinaKeystore.p12" +mount_dir="/home/ballerina/resource1" + +``` +3. Kubernetes YAML file segment ```yaml apiVersion: "v1" kind: "Secret" diff --git a/gradle.properties b/gradle.properties index 8865d2bc..89043c94 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ org.gradle.caching=true org.gradle.jvmargs='-Dfile.encoding=UTF-8' group=io.ballerina -version=2.12.0-SNAPSHOT +version=3.0.0-SNAPSHOT systemProp.org.gradle.internal.publish.checksums.insecure=true ballerinaLangVersion=2201.9.0-20240201-152800-3c3df044 stdlibConstraintVersion=1.4.0-20230831-142400-50e4023