Skip to content

Commit

Permalink
config: Transformer for old version of config
Browse files Browse the repository at this point in the history
Refs: #2130
- Generic transformer for cnf-testsuite.yml configs to newer versions.
- Extendable through addition of new transformation rules.
- The current functionality transforms to configv2, the structure of which
was proposed in #2129.

Signed-off-by: svteb <[email protected]>
  • Loading branch information
svteb authored and Konstantin Yarovoy committed Sep 16, 2024
1 parent ae06b22 commit 5b583ae
Show file tree
Hide file tree
Showing 6 changed files with 367 additions and 0 deletions.
8 changes: 8 additions & 0 deletions CNF_TESTSUITE_YML_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ Prereqs: You must have kubernetes cluster, curl, and helm 3.1.1 or greater on yo
- Generate a cnf-testsuite.yml based on a directory of manifest files: `./cnf-testsuite generate_config config-src=<your-manifest-directory> output-file=./cnf-testsuite.yml`
- Inspect the cnf-testsuite.yml file for accuracy

### Transformer for prior config versions

New releases may change the format of cnf-testsuite.yml, to quickly update your older configs to latest version use the `transform_config` task.

```
./cnf-testsuite transform_config old_config_path=OLD_CONFIG_PATH new_config_path=NEW_CONFIG_PATH
```

### Keys and Values

#### allowlist_helm_chart_container_names
Expand Down
37 changes: 37 additions & 0 deletions src/tasks/transform_config.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
require "sam"
require "totem"
require "colorize"
require "./utils/cnf_installation/transformer/config_transformer"

desc "Converts an old configuration file to the latest version and saves it to the specified location"
task "transform_config" do |_, args|
# Ensure both arguments are provided
if !((args.named.keys.includes? "old_config_path") && (args.named.keys.includes? "new_config_path"))
stdout_warning "Usage: transform_config old_config_path=OLD_CONFIG_PATH new_config_path=NEW_CONFIG_PATH"
exit(1)
end

old_config_path = args.named["old_config_path"].as(String)
new_config_path = args.named["new_config_path"].as(String)

# Check if the old config file exists
unless File.exists?(old_config_path)
stdout_failure "Error: The old config file '#{old_config_path}' does not exist."
exit(1)
end

begin
# Initialize the ConfigTransformer
transformer = CNFInstall::Config::ConfigTransformer.new(old_config_path)
transformer.transform

# Serialize the transformed config to the new file
transformer.serialize_to_file(new_config_path)

stdout_success "Configuration successfully transformed and saved to '#{new_config_path}'."
rescue ex : CNFInstall::Config::UnsupportedConfigVersionError
stdout_failure ex.message
rescue ex : Exception
stdout_failure "Unexpected error: #{ex.class}"
end
end
68 changes: 68 additions & 0 deletions src/tasks/utils/cnf_installation/config_versions/config_v1.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
module CNFInstall
module Config
@[YAML::Serializable::Options(emit_nulls: true)]
class ConfigV1 < ConfigBase
getter config_version : String?
getter destination_cnf_dir : String?
getter source_cnf_dir : String?
getter manifest_directory : String?
getter helm_directory : String?
getter release_name : String?
getter helm_repository : HelmRepository?
getter helm_chart : String?
getter helm_values : String?
getter helm_install_namespace : String?
getter container_names : Array(Container)?
getter white_list_container_names : Array(String)?
getter docker_insecure_registries : Array(String)?
getter image_registry_fqdns : Hash(String, String?)?

# Unused properties
getter install_script : String?
getter service_name : String?
getter git_clone_url : String?
getter docker_repository : String?

# 5G related properties
getter amf_label : String?
getter smf_label : String?
getter upf_label : String?
getter ric_label : String?
getter core : String?
getter amf_service_name : String?
getter mmc : String?
getter mnc : String?
getter sst : String?
getter sd : String?
getter tac : String?
getter protectionScheme : String?
getter publicKey : String?
getter publicKeyId : String?
getter routingIndicator : String?
getter enabled : String?
getter count : String?
getter initialMSISDN : String?
getter key : String?
getter op : String?
getter opType : String?
getter type : String?
getter apn : String?
getter emergency : String?

# Nested class for Helm Repository details
class HelmRepository < ConfigBase
getter name : String?
getter repo_url : String?
end

# Nested class for Container details
class Container < ConfigBase
getter name : String?
getter rollback_from_tag : String?
getter rolling_update_test_tag : String?
getter rolling_downgrade_test_tag : String?
getter rolling_version_change_test_tag : String?
end
end
end
end
97 changes: 97 additions & 0 deletions src/tasks/utils/cnf_installation/transformer/config_transformer.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
require "yaml"

module CNFInstall
module Config
# REQUIRES FUTURE EXTENSION in case of new config format.
enum ConfigVersion
V1
V2
Latest = V2
end

class ConfigTransformer
@new_config : YAML::Any
@old_config : ConfigBase
@version : ConfigVersion

# The initializer reads the config file twice to determine the config version
# of @old_config, this cannot be removed due to later parsing to appropriate class.
def initialize(old_config_path : String)
tmp_content = File.read(old_config_path)
yaml_content = YAML.parse(tmp_content).as_h

# Automatic version detection to streamline the transformation
@version = detect_version(yaml_content)
if @version == ConfigVersion::Latest
stdout_warning "Your config is the latest version."
exit(1)
end

@new_config = YAML::Any.new({} of YAML::Any => YAML::Any)
@old_config = parse_old_config(tmp_content)
end

# Serialize the transformed config to a string.
def serialize_to_string : String
YAML.dump(@new_config)
end

# Serialize the transformed config to a file and return the file path.
def serialize_to_file(file_path : String) : String
File.write(file_path, serialize_to_string)
file_path
end

# Detects the config version.
# REQUIRES FUTURE EXTENSION in case of new config format.
private def detect_version(yaml_content : Hash(YAML::Any, YAML::Any)) : ConfigVersion
version_value = yaml_content["config_version"]?.try(&.to_s)

if version_value
begin
ConfigVersion.parse(version_value.upcase)
rescue ex : Exception
raise UnsupportedConfigVersionError.new(version_value)
end
else
ConfigVersion::V1
end
end

# Parses the config to the correct class.
# REQUIRES FUTURE EXTENSION in case of new config format.
private def parse_old_config(tmp_content : String) : ConfigBase
begin
case @version
when ConfigVersion::V1
ConfigV1.from_yaml(tmp_content)
else
raise UnsupportedConfigVersionError.new(@version)
end
rescue ex : YAML::ParseException
# This is usually raised in case of unexpected YAML keys.
stdout_failure "Failed to parse config: #{ex.message}."
stdout_failure "Please check your YAML fields for correctness."
exit(1)
end
end

# Performs the transformation from V1 to V2.
# REQUIRES FUTURE EXTENSION in case of new config format.
def transform
case @version
when ConfigVersion::V1
@new_config = V1ToV2Transformation.new(@old_config.as(ConfigV1)).transform
else
raise UnsupportedConfigVersionError.new(@version)
end
end
end

class UnsupportedConfigVersionError < Exception
def initialize(version : ConfigVersion | String)
super "Unsupported configuration version: #{version.is_a?(ConfigVersion) ? version.to_s.downcase : version}"
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module CNFInstall
module Config
# The rules need to be somewhat explicit, different approaches have been attempted
# but due to crystals strict typing system they have not been viable/would be too complicated.
#
# In case of future extension, create a new transformation rules class (VxToVyTransformation),
# This class should inherit the TransformationBase class and make use of process_data
# function at the end of its transform function.
class TransformationBase
@new_config : YAML::Any

def initialize(@old_config : ConfigV1)
@new_config = YAML::Any.new({} of YAML::Any => YAML::Any)
end

# Recursively remove any empty hashes/arrays/values and convert data to YAML::Any.
private def process_data(data : Hash | Array | String | Nil) : YAML::Any?
case data
when Array
processed_array = data.map { |item| process_data(item) }.compact
processed_array.empty? ? nil : YAML::Any.new(processed_array)
when Hash
processed_hash = Hash(YAML::Any, YAML::Any).new
data.each do |k, v|
processed_value = process_data(v)
processed_hash[YAML::Any.new(k)] = processed_value unless processed_value.nil?
end
processed_hash.empty? ? nil : YAML::Any.new(processed_hash)
when String
YAML::Any.new(data)
else
nil
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
module CNFInstall
module Config
# Rules for configV1 to configV2 transformation
class V1ToV2Transformation < TransformationBase
def transform : YAML::Any
new_config_hash = {
"config_version" => "v2",
"common_parameters" => transform_common_parameters,
"dynamic_parameters" => transform_dynamic_parameters,
"deployments" => transform_deployments,
}

# Convert the entire native hash to stripped YAML::Any at the end.
@new_config = process_data(new_config_hash).not_nil!
end

private def transform_common_parameters : Hash(String, Array(Hash(String, String | Nil)) | Array(String) | Hash(String, String | Nil))
common_params = {} of String => Array(Hash(String, String | Nil)) | Array(String) | Hash(String, String | Nil)

common_params = {
"white_list_container_names" => @old_config.white_list_container_names,
"docker_insecure_registries" => @old_config.docker_insecure_registries,
"image_registry_fqdns" => @old_config.image_registry_fqdns,
"container_names" => transform_container_names,
"five_g_parameters" => transform_five_g_parameters
}.compact

common_params
end

private def transform_container_names : Array(Hash(String, String | Nil))
if @old_config.container_names
containers = @old_config.container_names.not_nil!.map do |container|
{
"name" => container.name,
"rollback_from_tag" => container.rollback_from_tag,
"rolling_update_test_tag" => container.rolling_update_test_tag,
"rolling_downgrade_test_tag" => container.rolling_downgrade_test_tag,
"rolling_version_change_test_tag" => container.rolling_version_change_test_tag
}
end

return containers
end

[] of Hash(String, String | Nil)
end

private def transform_dynamic_parameters : Hash(String, String | Nil)
{
"source_cnf_dir" => @old_config.source_cnf_dir,
"destination_cnf_dir" => @old_config.destination_cnf_dir
}
end

private def transform_deployments : Hash(String, Array(Hash(String, String | Nil)))
deployments = {} of String => Array(Hash(String, String | Nil))

if @old_config.manifest_directory
deployments["manifests"] = [{
"name" => @old_config.release_name,
"manifest_directory" => @old_config.manifest_directory
}]
elsif @old_config.helm_directory
deployments["helm_directories"] = [{
"name" => @old_config.release_name,
"helm_directory" => @old_config.helm_directory,
"helm_values" => @old_config.helm_values,
"namespace" => @old_config.helm_install_namespace
}]
elsif @old_config.helm_chart
helm_chart_data = {
"name" => @old_config.release_name,
"helm_chart_name" => @old_config.helm_chart,
"helm_values" => @old_config.helm_values,
"namespace" => @old_config.helm_install_namespace
}

if @old_config.helm_repository
helm_chart_data["helm_repo_name"] = @old_config.helm_repository.not_nil!.name
helm_chart_data["helm_repo_url"] = @old_config.helm_repository.not_nil!.repo_url
end

deployments["helm_charts"] = [helm_chart_data]
end

deployments
end

private def transform_five_g_parameters : Hash(String, String | Nil)
{
"core" => @old_config.core,
"amf_label" => @old_config.amf_label,
"smf_label" => @old_config.smf_label,
"upf_label" => @old_config.upf_label,
"ric_label" => @old_config.ric_label,
"amf_service_name" => @old_config.amf_service_name,
"mmc" => @old_config.mmc,
"mnc" => @old_config.mnc,
"sst" => @old_config.sst,
"sd" => @old_config.sd,
"tac" => @old_config.tac,
"protectionScheme" => @old_config.protectionScheme,
"publicKey" => @old_config.publicKey,
"publicKeyId" => @old_config.publicKeyId,
"routingIndicator" => @old_config.routingIndicator,
"enabled" => @old_config.enabled,
"count" => @old_config.count,
"initialMSISDN" => @old_config.initialMSISDN,
"key" => @old_config.key,
"op" => @old_config.op,
"opType" => @old_config.opType,
"type" => @old_config.type,
"apn" => @old_config.apn,
"emergency" => @old_config.emergency
}
end
end
end
end

0 comments on commit 5b583ae

Please sign in to comment.