
# NAME

OcToolkit - Open Cloud Toolkit

# SYNOPSIS

    use OcToolkit;
    
    my $ocObj = OcToolkit->new( 
                            advanceFeatures       => $advanceFeatures,
                            clusterBaseAddress    => $clusterBaseAddress,
                            cluster               => $cluster,
                            ocConfigFile          => $ocConfigFile,
                            host                  => $host,
                            ocResourceKinds       => $ocResourceKinds,
                            componentDirs         => $componentDirs,
                            namespace             => $namespace,
                            projectName           => $projectName,
                            omit                  => $omit,
                            urlPrefix             => $urlPrefix,
                            clusterIpRange        => $clusterIpRange,
                            secretsDir            => $secretsDir,
                            sortType              => $sortType,
                            templatesTTDir        => $templatesTTDir,
                            yamlToTTconvertDir    => $yamlToTTconvertDir,
                            specificYamlFile      => $specificYamlFile,
                            templatesYamlDir      => $templatesYamlDir,
                            addFlagValuesToConfig => \&addFlagValuesToConfig,
                            componentIsAllowed    => \&componentIsAllowed,
                            generateUrl           => \&generateUrl,
                            removeClutter         => \&removeClutter,
                            removeClutterBackup   => \&removeClutterBackup);
    $ocObj->install('test');
    $ocObj->validate('test');
    $ocObj->update('test');
    $ocObj->backup('prod');
    $ocObj->delete('dev');

# DESCRIPTION

Helm like tool for Openshift and Kubernetes with multi cluster support.
See https://gitlab.com/code7143615/octoolkit/-/blob/master/README.md how to use this library in ocToolkit.pl script
and use it as 'Helm-like' command line tool

# LICENSE

Copyright (C) John Summers.

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

# AUTHOR

John Summers <devp2000a@gmail.com>

# SCRIPT TO RUN IT

\#!/usr/bin/perl

use strict;
use warnings;

\# search for Module in same directory where this script is
use FindBin;
use File::Spec;
use lib File::Spec->catdir($FindBin::Bin);

use Getopt::Std;
use OcToolkit;
use Data::Dumper;

my $help = "Install/uninstall/backup/validate/upgrade instances and they components into/from Openshift/Kubernetes cluster
Put your templates in 'templates\_tt' dir located in current dir. Every component should have own dir(10-api,20-solr,...) inside of it.
Write your templates with Template Toolkit(https://template-toolkit.org) templating engine.
Put your config into oc\_config.json. Located in current dir.
   'instance\_specific\_data' : put your instance specific data in this json node, see example belove and in git
   'instance\_specific\_name' : instance string will be automatically added at the end of every entry, see example belove and in git
   'project': project specifc data
   oc\_config.json root data are fix for every instance

Flags description:
  -A cluster base address, e.g.: 'apps.clusterintern' or 'apps.clusterpublic', used in default route url generation
  -a advance features:
    '-a multipleClusters'  if cluster specific templates are required, put them into nested dir
                           e.g.: templates\_tt/50-api/clusterIntern/50-deployment-config-api.tt, 
                           ocToolkit will automatically select corresponding templates
                           (currently this flag is selected by default)
     '-a kubectl'          use Kubernetes 'kubectl' instead of default Openshift 'oc' command
     '-a removeClutter'    remove clutter yaml fields during backup
    Use multiple: -a 'multipleClusters;kubectl;removeClutter'
  -b instance name(s), backups specific instance(s) sorted by components e.g.: -b 'dev;test' or -b all to backup whole project(unsorted) 
     if '-a multipleClusters' and not '-b all' is used then '-c' flag is mandatory
  -c cluster name e.g.: 'clusterIntern','clusterPublic'... Default is defined in oc\_config.json->project->default\_cluster. 
     You should be logged in into corresponding cluster
     If cluster label in url should be different then cluster name, use '-A' flag to set custom cluster url label
  -C config file, default: 'oc\_config.json'
  -d instanceName(s), deletes instances from logged in project e.g.: '-d test', see -i documentation
  -h help, prints this help
  -H host, used in default route url generation
  -i instance name(s) e.g.: installs given instances e.g.: 'test;prod'
  -k use(install/delete/validate/upgrade/generate yamls/backup) only specific Openshift/Kubernetes resource kind(s) 
     e.g.: -k 'DeploymentConfig;Service;Route' Default is defined in oc\_config.json->project->oc\_resource\_kinds
  -m component directory names e.g.: 'init-project;init-api;init-gateway;solr;api;gateway;public-ui;admin-ui;swagger;cron-jobs'
     omit flag or use '-m all' to select all defined in oc\_config.json->project->component\_dirs
  -n openshift project namespace e.g.: 'myNameSpace' if not set, current oc project name will be used as default namespace
  -N project name, used in default route url generation
  -o 'init', all directories which name string includes 'init' will be omitted,
           useful if you like to preserve data volumes during install/delete operations
     'oc', only yaml files will be generated
  -p url prefix, adds prefix to all route urls, useful when running in Openshift sandbox in oder to avoid network routes conflicts
  -r openshift cluster IP range(first three numbers) e.g.: '112.20.14', last number will be randomly generated
  -s use this flag to change secrets directory. Default is 'secrets'
  -S 'numeric' or 'alphabetic', dirs and files sort type(relevant for running order), default is 'numeric'
  -t set custom 'templates\_tt' directory
  -T directoryName, convert '.yaml' files extension into '.tt' extention inside of given directory
  -u instanceName(s), runs validation, creates 'validation\_report.txt' und then runs upgrade of components that are modified,
     e.g.: -u test
  -v instanceName(s), validates given instance(s) between template version and Openshift version in cloud e.g.: -v test, 
     report is written in 'validation\_report.txt' file
  -y use(install/delete/validate/upgrade/generate yamls) only for yaml files that includes given substring
  -Y set custom 'templates\_yaml' directory

    see '-a multipleClusters' flag
    oc_config.json magic nodes:
        'instance_specific_data': instance will be automatically selected, e.g.: for json node 'instance_specific_data.api.test.limits.memory'  
                                  you can access instance specific data in tt template by 
                                  [% oc_config.instance_specific_data.api.limits.memory %] if current instance is 'test' 
        'instance_specific_name': '-instanceName' will be added at the end of each value, e.g.: if you have json node 'instance_specific_name.api'
                                  with value 'my-api' then by accessing [% oc_config.instance_specific_name.api %] in tt template, in yaml file will be 
                                  written 'my-api-test' if current instance is 'test
    see more examples in git: https://gitlab.com/code7143615/octoolkit/-/tree/master
    
    Examples: 
          # install 'dev' and 'test' instances in 'clusterIntern' cluster
          # you shoud be logged in in same cluster that you specified in '-c' flag
          ocToolkit -c clusterIntern -i 'dev;test' 
          
          # deletes 'api' and 'solr' components on 'test' instance in currently logged in project
          ocToolkit -d test -m 'api;solr'
          
          # backups all components on 'test' instance in logged in project and remove clutter yaml nodes
          ocToolkit -b test -a removeClutter
          
          # validates 'api' and 'solr' components on 'test' instance in logged in project
          # you shoud be logged in in same cluster that you specified in '-c' flag
          ocToolkit -c clusterIntern -v test -m 'api;solr'
          
          # upgrades 'solr' component on 'test' instance in logged in project but all dirs that have 'init' string in its name will me omitted
          # you shoud be logged in in same cluster that you specified in '-c' flag
          ocToolkit -c clusterIntern -u test -m solr -o init
          
          # generate yaml templates(no installing) for oc resourse kinds 'DeploymentConfig' and 'Service' 
          # of 'solr' component for 'test' instance for 'clusterPublic' cluster
          ocToolkit -c clusterPublic -i test -m solr -k 'DeploymentConfig;Service' -o oc
    
    Place instance specific secretes in e.g.: secrets/instance/test/my_secret.txt for 'test' instance and 
    access them by [% secrets.instance_specific.item('my_secret.txt') %] from tt template
    Make sure that oc_config.json->project node values are set correctly.

    ";
    

sub addFlagValuesToConfig($);
sub componentIsAllowed($$$$);
sub generateUrl($$$$$$);
sub removeClutter($$);
sub \_loopInstances($$);

my $clusterBaseAddress;
my $advanceFeatures;
my $backupSpecificInstances;
my $cluster = "clusterPublic"; # default
my $ocConfigFile;
my $deleteInstances;
my $host;
my $installInstances;
my $ocResourceKinds;
my $componentDirs;
my $namespace;
my $projectName; # used in default url generation
my $omit   = ""; 
my $urlPrefix;
my $clusterIpRange;
my $secretsDir;
my $sortType;
my $templatesTTDir;
my $yamlToTTconvertDir;
my $upgradeInstances;
my $validateInstances;
my $specificYamlFile;
my $templatesYamlDir;

my %opts;
getopts("a:A:b:c:d:H:h:i:k:m:M:n:N:o:O:p:r:s:S:t:T:u:v:y:Y:", \\%opts);

if($opts{h}){
    print $help;
    exit;
}

$clusterBaseAddress      = $opts{A} if defined $opts{A};
$advanceFeatures         = $opts{a} if defined $opts{a};
$backupSpecificInstances = $opts{b} if defined $opts{b};
$cluster                 = $opts{c} if defined $opts{c};
$ocConfigFile            = $opts{C} if defined $opts{C};
$deleteInstances         = $opts{d} if defined $opts{d};
$host                    = $opts{H} if defined $opts{H};
$installInstances        = $opts{i} if defined $opts{i};
$ocResourceKinds         = $opts{k} if defined $opts{k};
$componentDirs           = $opts{m} if (defined $opts{m}) && ($opts{m} ne "all");
$namespace               = $opts{n} if defined $opts{n};
$projectName             = $opts{N} if defined $opts{N};
$omit                    = $opts{o} if defined $opts{o};
$urlPrefix               = $opts{p} if defined $opts{p};
$clusterIpRange          = $opts{r} if defined $opts{r};
$secretsDir              = $opts{s} if defined $opts{s};
$sortType                = $opts{S} if defined $opts{S};
$templatesTTDir          = $opts{t} if defined $opts{t};
$yamlToTTconvertDir      = $opts{T} if defined $opts{T};
$upgradeInstances        = $opts{u} if defined $opts{u};
$validateInstances       = $opts{v} if defined $opts{v};
$specificYamlFile        = $opts{y} if defined $opts{y};
$templatesYamlDir        = $opts{Y} if defined $opts{Y};

if(defined $installInstances){
    die("Please set working cluster with -c flag, e.g.: perl ocToolkit.pl -c clusterPublic -i prod") if not defined $cluster;
}

my $flag;
$flag = $deleteInstances         if defined $deleteInstances;
$flag = $installInstances        if defined $installInstances;
$flag = $backupSpecificInstances if defined $backupSpecificInstances;
$flag = $validateInstances       if defined $validateInstances;
$flag = $upgradeInstances        if defined $upgradeInstances;
die("Flags -i -d -b -v -u can't be left empty.") if (defined $flag) && (length($flag) eq 2) && ($flag =~ /\\-/ ); 

if(not defined $clusterBaseAddress){
    my $clusterLc = lc $cluster;
    $clusterBaseAddress = "apps.$clusterLc";
}

if(defined $deleteInstances){
    my $myComponentDirs = $componentDirs;
    if($omit =~ /init/ && (defined $componentDirs)){
        # remove init dirs
        my @componentsDirArray = split(';', $componentDirs);
        @componentsDirArray    = (grep {$\_ !~ /init/} @componentsDirArray);
        $myComponentDirs       = join( ';', @componentsDirArray);
    }
    $myComponentDirs = "all" if not defined $myComponentDirs;
    print qx/oc project/;
    print "Deleting $myComponentDirs component(s) in '$deleteInstances' instance in '$cluster' cluster.
    \\nPress enter to continue or ctrl+c to abort";
    my $continue = <>;
}

\############################################################################
\# install/delete/validate/update/backup/create yamls for specific instance #
\############################################################################

\# set 'multipleClusters' as default
if(not defined $advanceFeatures){
    $advanceFeatures = "multipleClusters";
}else{
    $advanceFeatures .= ";multipleClusters";
}
my $ocObj = OcToolkit->new( advanceFeatures       => $advanceFeatures,
                            clusterBaseAddress    => $clusterBaseAddress,
                            cluster               => $cluster,
                            ocConfigFile          => $ocConfigFile,
                            host                  => $host,
                            ocResourceKinds       => $ocResourceKinds,
                            componentDirs         => $componentDirs,
                            namespace             => $namespace,
                            projectName           => $projectName,
                            omit                  => $omit,
                            urlPrefix             => $urlPrefix,
                            clusterIpRange        => $clusterIpRange,
                            secretsDir            => $secretsDir,
                            sortType              => $sortType,
                            templatesTTDir        => $templatesTTDir,
                            yamlToTTconvertDir    => $yamlToTTconvertDir,
                            specificYamlFile      => $specificYamlFile,
                            templatesYamlDir      => $templatesYamlDir,
                            addFlagValuesToConfig => \\&addFlagValuesToConfig,
                            componentIsAllowed    => \\&componentIsAllowed,
                            generateUrl           => \\&generateUrl,
                            removeClutter         => \\&removeClutter,
                            removeClutterBackup   => \\&removeClutterBackup);

\# $ocObj->setParams({omit => "oc"});
\_loopInstances($deleteInstances,         "delete")   if defined $deleteInstances;
\_loopInstances($installInstances,        "install")  if defined $installInstances;
\_loopInstances($upgradeInstances,        "upgrade")  if defined $upgradeInstances;
\_loopInstances($validateInstances,       "validate") if defined $validateInstances;
if(defined $backupSpecificInstances){
    if($backupSpecificInstances eq "all"){
        $ocObj->backupWholeOCProject();
    }else{
        \_loopInstances($backupSpecificInstances, "backup");
    }
}
$ocObj->convertYamlToTTExtention($yamlToTTconvertDir) if defined $yamlToTTconvertDir;

\#####################################################################################
\# use this functions to add custom config/logic without need to change OcToolkit.pm # 
\#####################################################################################

\# add some script input flag value to config file, access them then in TT Template by \[% my\_custom\_value %\]
sub addFlagValuesToConfig($){
    my ($config) = @\_;

    $config->{my_custom_value} = "some custom value received from flag";

    return $config;
}

\# some specific rules about what components are allowed to be installed on given cluster and instance
\# if you need completely different tt template files for specific clusters use '-a multipleClusters' flag
\# put tt template file into nested directory, named by your cluster, inside of your tt template component dirs
sub componentIsAllowed($$$$){
    my ($myTemplateName, $myDir, $myCluster, $myInstance) = @\_;

\#     my $clusterLowerCase = lc $myCluster;
\#     if($clusterLowerCase =~ /clusterPublic/){
\#         if($myTemplateName =~ /route/){
\#             return 0 if $myDir =~ /solr/;
\#             return 0 if $myDir =~ /api/;
\#             return 0 if $myDir =~ /admin\\-ui/;
\#         }
\#         return 0 if $myDir =~ /swagger/;
\#     }

    return 1;
}

\# define default routes url pattern
sub generateUrl($$$$$$){
    my ($urlPrefix, $projectName, $componentName, $instanceKey, $clusterBaseAddress, $host) = @\_;

    if(defined $urlPrefix){
        $urlPrefix .= "-";
    }else{
        $urlPrefix = "";
    }
    if(defined $projectName){
        $projectName .= "-";
    }else{
        $projectName = "";
    }
    if(defined $clusterBaseAddress){
        $clusterBaseAddress .= ".";
    }else{
        $clusterBaseAddress = "";
    }

    return $urlPrefix.$projectName.$componentName."-".$instanceKey.".".$clusterBaseAddress.$host;
}

\# ignore some specific fields during validation and upgrade
sub removeClutter($$){
    my ($ocJsonHash, $params) = @\_;

    $ocJsonHash = removeClutterBackup($ocJsonHash, $params);
    
    my $dir          = $params->{dir};
    my $templateName = $params->{templateName};
    my $ocKind       = $params->{ocKind};
    my $ocName       = $params->{ocName};

    # some extra fields(in comparison to backup) to be ignored during validation and upgrade
    foreach my $i ((0..7)){
        if($ocKind eq "BuildConfig"){
            if((defined $ocJsonHash->{spec}) &&
               (defined $ocJsonHash->{spec}->{triggers}) &&
               (defined $ocJsonHash->{spec}->{triggers}->[$i]) &&
               (defined $ocJsonHash->{spec}->{triggers}->[$i]->{github}) &&
               (defined $ocJsonHash->{spec}->{triggers}->[$i]->{github}->{secret})
            ){
                delete $ocJsonHash->{spec}->{triggers}->[$i]->{github}->{secret};
            }
            if((defined $ocJsonHash->{spec}) &&
               (defined $ocJsonHash->{spec}->{triggers}) &&
               (defined $ocJsonHash->{spec}->{triggers}->[$i]) &&
               (defined $ocJsonHash->{spec}->{triggers}->[$i]->{generic}) &&
               (defined $ocJsonHash->{spec}->{triggers}->[$i]->{generic}->{secret})
            ){
                delete $ocJsonHash->{spec}->{triggers}->[$i]->{generic}->{secret};
            }
        }
        if($ocKind eq "ImageStream"){
            if((defined $ocJsonHash->{spec}) &&
               (defined $ocJsonHash->{spec}->{tags}) &&
               (defined $ocJsonHash->{spec}->{tags}->[$i]) &&
               (defined $ocJsonHash->{spec}->{tags}->[$i]->{importPolicy})
            ){
                delete $ocJsonHash->{spec}->{tags}->[$i]->{importPolicy};
            }
        }
        if($ocKind eq "DeploymentConfig"){
            if((defined $ocJsonHash->{spec}) &&
               (defined $ocJsonHash->{spec}->{template}) &&
               (defined $ocJsonHash->{spec}->{template}->{spec}) &&
               (defined $ocJsonHash->{spec}->{template}->{spec}->{containers}) &&
               (defined $ocJsonHash->{spec}->{template}->{spec}->{containers}->[$i]) &&
               (defined $ocJsonHash->{spec}->{template}->{spec}->{containers}->[$i]->{resources}) &&
               (not defined $ocJsonHash->{spec}->{template}->{spec}->{containers}->[$i]->{resources}->{limits}) &&
               (not defined $ocJsonHash->{spec}->{template}->{spec}->{containers}->[$i]->{resources}->{requests})
            ){
                $ocJsonHash->{spec}->{template}->{spec}->{containers}->[$i]->{resources} = {};
            }
        }
    }

    delete $ocJsonHash->{spec}->{clusterIPs};
    delete $ocJsonHash->{spec}->{clusterIP};
    delete $ocJsonHash->{spec}->{volumeName} if $ocKind eq "PersistentVolumeClaim";
    
    return $ocJsonHash;
}

\# ignore some specific fields during backup of the whole project
sub removeClutterBackup($$){
    my ($ocJsonHash, $params) = @\_;

    my $dir          = $params->{dir};
    my $templateName = $params->{templateName};
    my $ocKind       = $params->{ocKind};
    my $ocName       = $params->{ocName};
    
    delete $ocJsonHash->{status};
    
    delete $ocJsonHash->{metadata}->{annotations}->{'openshift.io/generated-by'};
    delete $ocJsonHash->{metadata}->{annotations}->{'openshift.io/restore-server-version'};
    delete $ocJsonHash->{metadata}->{annotations}->{'openshift.io/backup-server-version'};
    delete $ocJsonHash->{metadata}->{annotations}->{'openshift.io/backup-registry-hostname'};
    delete $ocJsonHash->{metadata}->{annotations}->{'openshift.io/migration-registry'};
    delete $ocJsonHash->{metadata}->{annotations}->{'openshift.io/restore-registry-hostname'};
    delete $ocJsonHash->{metadata}->{annotations}->{'openshift.io/image.dockerRepositoryCheck'};
    delete $ocJsonHash->{metadata}->{annotations}->{'kubectl.kubernetes.io/last-applied-configuration'};
    delete $ocJsonHash->{metadata}->{annotations}->{'kubernetes.io/service-account.name'};
    delete $ocJsonHash->{metadata}->{annotations}->{'kubernetes.io/service-account.uid'};
    delete $ocJsonHash->{metadata}->{annotations}->{'openshift.io/token-secret.value'};
    delete $ocJsonHash->{metadata}->{annotations}->{'openshift.io/token-secret.name'};
    delete $ocJsonHash->{metadata}->{annotations}->{'kubernetes.io/created-by'};
    delete $ocJsonHash->{metadata}->{annotations}->{'volume.beta.kubernetes.io/storage-provisioner'};
    delete $ocJsonHash->{metadata}->{annotations}->{'pv.kubernetes.io/bind-completed'};
    delete $ocJsonHash->{metadata}->{annotations}->{'pv.kubernetes.io/bound-by-controller'};
    delete $ocJsonHash->{metadata}->{annotations}->{'volume.kubernetes.io/storage-provisioner'};
    
    delete $ocJsonHash->{metadata}->{labels}->{'migration.openshift.io/migrated-by-migmigration'};
    delete $ocJsonHash->{metadata}->{labels}->{'migration.openshift.io/migrated-by-migplan'};
    delete $ocJsonHash->{metadata}->{labels}->{'velero.io/backup-name'};
    delete $ocJsonHash->{metadata}->{labels}->{'velero.io/restore-name'};
    delete $ocJsonHash->{metadata}->{labels}->{'app.kubernetes.io/component'};
    delete $ocJsonHash->{metadata}->{labels}->{'app.kubernetes.io/instance'};
    
    delete $ocJsonHash->{metadata}->{resourceVersion};
    delete $ocJsonHash->{metadata}->{uid};
    delete $ocJsonHash->{metadata}->{creationTimestamp};
    delete $ocJsonHash->{metadata}->{generation};
    delete $ocJsonHash->{metadata}->{managedFields};
    delete $ocJsonHash->{metadata}->{selfLink};
    delete $ocJsonHash->{metadata}->{deletionTimestamp};
    delete $ocJsonHash->{metadata}->{deletionGracePeriodSeconds};

    foreach my $i ((0..7)){
        if((defined $ocJsonHash->{spec}) &&
           (defined $ocJsonHash->{spec}->{triggers}) &&
           (defined $ocJsonHash->{spec}->{triggers}->[$i]) &&
           (defined $ocJsonHash->{spec}->{triggers}->[$i]->{imageChangeParams}) &&
           (defined $ocJsonHash->{spec}->{triggers}->[$i]->{imageChangeParams}->{lastTriggeredImage})
        ){
            delete $ocJsonHash->{spec}->{triggers}->[$i]->{imageChangeParams}->{lastTriggeredImage};
        }
        
        if((defined $ocJsonHash->{spec}) &&
           (defined $ocJsonHash->{spec}->{tags}) &&
           (defined $ocJsonHash->{spec}->{tags}->[$i]) &&
           (defined $ocJsonHash->{spec}->{tags}->[$i]->{generation})
        ){
            delete $ocJsonHash->{spec}->{tags}->[$i]->{generation};
        }
        
        if((defined $ocJsonHash->{spec}) &&
           (defined $ocJsonHash->{spec}->{template}) &&
           (defined $ocJsonHash->{spec}->{template}->{spec}) &&
           (defined $ocJsonHash->{spec}->{template}->{spec}->{containers}) &&
           (defined $ocJsonHash->{spec}->{template}->{spec}->{containers}->[$i]) &&
           (defined $ocJsonHash->{spec}->{template}->{spec}->{containers}->[$i]->{image})
        ){
            delete $ocJsonHash->{spec}->{template}->{spec}->{containers}->[$i]->{image};
        }
    }

    return $ocJsonHash;
}

sub \_loopInstances($$){
    my ($instancesString, $methodName) = @\_;

    my @instances =  split(';', $instancesString);
     foreach my $instance (@instances){
        my $methodNameU = ucfirst $methodName;
        print "$methodNameU instance: $instance\n";
        $ocObj->$methodName($instance) if $ocObj->can($methodName); 
    }
}

1;
