Swagger-baser server REST API interface
[ric-plt/appmgr.git] / scripts / appmgrcli
1 #!/usr/bin/perl -w
2 #
3 # Copyright (c) 2019 AT&T Intellectual Property.
4 # Copyright (c) 2019 Nokia.
5 #
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
9 #
10 #     http://www.apache.org/licenses/LICENSE-2.0
11 #
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 #
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
18 #
19 #############################
20 # Simple cli for xapp manager
21 #
22 # In addition to standard shell tools, requires basic Perl installation
23 # (Ubuntu package "perl-base", installed by default), packages "curl" and
24 # "yajl-tools" (the second provides json_reformat on Ubuntu; on Red Hat-style
25 # distributions install "yajl" instead).
26 #
27 use strict;
28 use Getopt::Long;
29 use Fcntl;
30
31 my $myname="appmgrcli";
32
33 sub usage {
34     print <<"EOF1";
35 usage: $myname [-h host] [-p port] [-v] [-c curlprog] command params...
36 - command is deploy, undeploy, status, subscriptions, health, config, help
37 - (abbreviations dep, undep, stat, subs, heal allowed)
38 - Parameters of the commands that may have parameters:
39 -- deploy: name of the xapp to deploy
40 ---- Deployment parameters come from the Help chart but the following can be
41 ---- overridden by option with the same name (for example --configName=cname):
42 ----- helmVersion ReleaseName namespace podHost (host of pods).
43 -- undeploy: name of the xapp to undeploy
44 -- status:
45 ---- No parameters: Lists information about all deployed xapps
46 ---- xapp name as parameter: Prints information about the given xapp
47 ---- xapp name and instance: Lists information about the given instance only
48 -- subscriptions is followed by sub-command list, add, delete, or modify
49 --- (abbreviations del and mod for delete and modify are allowed):
50 ---- list without parameters lists all subscriptions
51 ---- list with subscription id prints that subscription
52 ---- add URL eventType maxRetry retryTimer
53 ------- URL is the URL to notify
54 ------- eventType one of created,deleted,all
55 ------- maxRetry and retryTimer are positive decimal numbers
56 ---- modify id URL eventType maxRetry retryTimer
57 ------- id is the subscription id (find out with the list command)
58 --------the rest of the parameters are like in add
59 ---- delete id
60 ------- id is the subscription id to delete (find out with the list command)
61 -- config is followed by sub-command list, add, delete, or modify
62 --- (abbreviations del and mod for delete and modify are allowed):
63 ---- list (no pars)
64 ------ lists the configuration of all xapps
65 ---- add jsonfile
66 ------ Creates xapp configuration. the jsonfile must contain all data (see API)
67 ---- add name configName namespace configSchemaFile configDataFile
68 ------ Creates xapp configuration, but unlike in the 1-parameter form,
69 ------ Xapp name, config map name and namespace are separate parameters.
70 ------ The other data come from JSON files.
71 ---- modify
72 ------ Modifies existing configuration. Same parameters (1 or 5) as in add.
73 ---- delete name configName namespace
74 ------ Deletes the configuration identified by the parameters.
75 --------------------------------------------------------------
76 - Default values for host and port can be set in environment
77 - variables APPMGR_HOST and APPMGR_PORT
78 - Option -v sets verbose mode.
79 - Option -c overrides the used curl program name ("curl" by default).
80 - Exit code is 0 for success, 1 for any kind of failure.
81 EOF1
82     exit 0;
83 }
84
85 sub helphint {
86     print "run $myname help (or --help) for instructions\n";
87 }
88
89 # Defaults
90
91 my $host="localhost";
92 my $port=8080;
93 my $verbose=0;
94 my $showhelp = 0;
95
96 # API URLs
97
98 my $base="/ric/v1";
99 my $base_xapps="$base/xapps";
100 my $base_health="$base/health";
101 my $base_subs="$base/subscriptions";
102 my $base_config="$base/config";
103
104 # Check for environment override
105 if (exists $ENV{"APPMGR_HOST"}) {
106    $host=$ENV{"APPMGR_HOST"};
107 }
108 if (exists $ENV{"APPMGR_PORT"}) {
109     $port=$ENV{"APPMGR_PORT"};
110 }
111
112 # Overrides for some deploy parameters
113
114 my $configName = "";
115 my $namespace = "ricxapp";
116 my $releaseName = "";
117 my $helmVersion = "0.0.1";
118 my $overrideFile = "";
119 my $podHost = "";
120
121 # The curl command can be overridden for testing with a dummy.
122
123 my $curl = "curl";
124
125 Getopt::Long::Configure("no_auto_abbrev", "permute");
126 if (! GetOptions("h=s" => \$host,
127                  "p=i" => \$port,
128                  "c=s" => \$curl,
129                  "ConfigName=s" => \$configName,
130                  "Namespace=s" => \$namespace,
131                  "ReleaseName=s" => \$releaseName,
132                  "HelmVersion=s" => \$helmVersion,
133                  "OverrideFile=s" => \$overrideFile,
134                  "podHost=s" => \$podHost,
135                  "help" => \$showhelp,
136                  "v" => \$verbose)) {
137     print "$myname: Error in options\n";
138     helphint();
139     exit 1;
140 }
141
142 if ($showhelp) {
143     usage();
144     exit 0;
145 }
146
147 if ($verbose) {
148     print "host = $host\n";
149     print "port = $port\n";
150     print "ConfigName = $configName\n";
151     print "Namespace = $namespace\n";
152     print "ReleaseName = $releaseName\n";
153     print "HelmVersion = $helmVersion\n";
154      print "OverrideFile = $overrideFile\n";
155     print "podHost = $podHost\n";
156     for (my $idx = 0; $idx <= $#ARGV; ++$idx) {
157         print "\$ARGV[$idx] = $ARGV[$idx]\n";
158     }
159 }
160
161 # Verify command and call handler function
162
163 my %commands = (
164     "deploy" => \&do_deploy,
165     "dep" => \&do_deploy,
166     "undeploy" => \&do_undeploy,
167     "undep" => \&do_undeploy,
168     "status" => \&do_status,
169     "stat" =>  \&do_status,
170     "subscriptions" => \&do_subscriptions,
171     "subs" =>  \&do_subscriptions,
172     "health" => \&do_health,
173     "heal" => \&do_health,
174     "config" => \&do_config,
175     "help" => \&usage
176 );
177
178 if ($#ARGV < 0) {
179     print "$myname: Missing command\n";
180     helphint();
181     exit 1;
182 }
183
184 # Variable status used for the return value of the whole script.
185 my $status = 0;
186
187 my $command = $ARGV[0];
188 shift;
189 if (exists $commands{$command}) {
190     # Call the handler function with the rest of the command line
191     $commands{$command}(@ARGV);
192     exit $status; # Default exit. A handler can exit also if more convenient
193 }
194 print "$myname: Unrecognised command $command\n";
195 helphint();
196 exit 1;
197
198 my $errfile;
199 my $resultfile;
200
201
202 sub make_temp_name($) {
203     my $tmpsuffix = "${$}${^T}";
204     return "$_[0].$tmpsuffix";
205 }
206
207 sub make_temps {
208     $errfile = make_temp_name("/tmp/appmgr_e");
209     $resultfile = make_temp_name("/tmp/appmgr_r");
210 }
211
212 sub remove_temps {
213     unlink ($errfile, $resultfile);
214 }
215
216 sub print_file($$) {
217     my $outputhandle = $_[0];
218     my $filename = $_[1];
219     my $buffer;
220     my $inhandle;
221     if (!open($inhandle, "<", $filename)) {
222         print $outputhandle "$myname print_file: cannot open $filename: $!\n";
223         return;
224     }
225     while (read($inhandle, $buffer, 4000) > 0) {
226         print $outputhandle $buffer;
227     }
228     close($inhandle);
229 }
230
231 # The HTTP protocol result code, filled in by rest().
232
233 my $http_code = "";
234
235 # Helper: Given a curl output file, extract the number from ##code line.
236 # return ERROR if file cannot be opened, or "" if no code found.
237
238 sub find_http_code($) {
239     my ($fh, $line, $code);
240     open($fh, "<", $_[0]) or return "ERROR";
241     while ($line = <$fh>) {
242         if ($line =~ /^##([0-9]+)/) {
243             return $1;
244         }
245     }
246     return "";
247 }
248
249 # Helper for command execution:
250 # Do a rest call with "curl": $1 = method, $2 = path (without host and port
251 # which come from variables), $3 data to POST if needed
252 # returns true (1) if OK, and any returned data is in $resultfile
253 # else 0, and error message from curl is in $errfile, which is printed
254 # before returning the 0.
255 #
256 # On curl options: --silent --show-error disables progress bar, but allows
257 # error messages. --connect-timeout 20 limits waiting for connection to
258 # 20 seconds. In practice connection will succeed almost immediately,
259 # or in the case of wrong address not at all.
260 # To get the http code, using -w with format. The result comes at the end
261 # of the output, so "decorating" it for easier filtering.
262 # The code is put to global $http_code.
263 #
264 sub rest($$_) {
265     my $method = $_[0];
266     my $path = $_[1];
267     my $data = $_[2] || "";
268     my $retval = 1;
269     my $http_status_file = make_temp_name("/tmp/appmgr_h");
270
271     # This redirects stderr (fd 2) to $errfile, but saving normal stderr
272     # so that if can be restored.
273     open(OLDERR, ">&", \*STDERR) or die "Can't dup STDERR: $!";
274     open(ERRFILE, ">", $errfile) or die "open errorfile failed";
275     open(STDERR, ">&", \*ERRFILE) or die "Can't dup ERRFILE: $!";
276
277     # This redirects stdout (fd 1) to $http_status_file, but saving original
278     # so that if can be restored.
279     open(OLDSTDOUT, ">&", \*STDOUT) or die "Can't dup STDOUT: $!";
280     open(HTTP_STATUS_FILE, ">", $http_status_file) or die "open http status file failed";
281     open(STDOUT, ">&", \*HTTP_STATUS_FILE) or die "Can't dup HTTP_STATUS_FILE: $!";
282
283     my @args = ($curl, "--silent", "--show-error", "--connect-timeout", "20",
284                 "--header", "Content-Type: application/json", "-X", $method,
285                 "-o", $resultfile, "-w", '\n##%{http_code}\n',
286                 "http://${host}:${port}${path}");
287     if ($data ne "") {
288         push(@args, "--data");
289         push(@args, $data);
290     }
291     if ($verbose) {
292         print OLDSTDOUT "Running: " . join(" ", @args) . "\n";
293     }
294     if (system(@args) == -1) {
295         print OLDSTDOUT "$myname: failed to execute @args\n";
296         $retval = 0;
297     }
298     elsif ($? & 127) {
299          printf OLDSTDOUT "$myname: child died with signal %d, %s coredump\n",
300              ($? & 127),  ($? & 128) ? 'with' : 'without';
301          $retval = 0;
302     }
303     else {
304         my $curl_exit_code = $? >> 8;
305         if ($curl_exit_code == 0) {
306             seek HTTP_STATUS_FILE, 0, 0; # Ensures flushing
307             $http_code = find_http_code($http_status_file);
308             if ($http_code eq "ERROR") {
309                 print OLDSTDOUT "$myname: failed to open temp file $http_status_file\n";
310                 $retval = 0;
311             }
312             elsif ($http_code eq "") {
313                 print OLDSTDOUT "$myname: curl failed to provide HTTP code\n";
314                 $retval = 0;
315             }
316             else {
317                 if ($verbose) {
318                     print OLDSTDOUT "HTTP status code = $http_code\n";
319                 }
320                 $retval = 1; # Interaction OK from REST point of view
321             }
322         }
323         else {
324             print_file(\*OLDSTDOUT, $errfile);
325             $retval = 0;
326         }
327     }
328     open(STDOUT, ">&", \*OLDSTDOUT) or die "Can't dup OLDSTDOUT: $!";
329     open(STDERR, ">&", \*OLDERR) or die "Can't dup OLDERR: $!";
330     unlink($http_status_file);
331     return $retval;
332 }
333
334 # Pretty-print a JSON file to stdout.
335 # (currently uses json_reformat command)
336 # Skips the ##httpcode line we make "curl"
337 # add in order to get access to the HTTP status.
338
339 sub print_json($) {
340     my $filename = $_[0];
341     my ($line, $inhandle, $outhandle);
342     if (!open($inhandle, "<", $filename)) {
343         print "$myname print_json: cannot open $filename: $!\n";
344         return;
345     }
346     if (!open($outhandle, "|json_reformat")) {
347         print "$myname print_json: cannot pipe to json_reformat: $!\n";
348         return;
349     }
350     while ($line = <$inhandle>) {
351         if (! ($line =~ /^##[0-9]+/)) {
352             print $outhandle $line;
353         }
354     }
355     close($outhandle);
356     close($inhandle);
357 }
358
359 # Append an entry like ","name":"value" to the first parameter, if "name"
360 # names a variable with non-empty value.
361 # Else returns the unmodified first parameter.
362
363 sub append_option($$) {
364     my $result = $_[0];
365     my $var = $_[1];
366     my $val = eval("\$$var");
367     if ($val ne "") {
368         $result = "$result,\"$var\":\"$val\"";
369     }
370     return $result;
371 }
372
373 # Command handlers
374 # Assumes the API currently implemented.
375 # Functions for each command below
376
377 # Deploy:
378 # The deploy command has one mandatory parameter "name" in the API,
379 # and several optional ones. Used mainly internally for testing, because
380 # they all override Helm chart values:
381 # "helmVersion": Helm chart version to be used
382 # "releaseName": The releas name of xApp visible in K8s
383 # "namespace":  Name of the namespace to which xApp is deployed.
384 # "overrideFile":  The file content used to override values.yaml file
385 # this host from the host the xapp manager is running in, we use the term
386 # and variable name "podHost" here.
387 # The options come from options (see GetOptions() call).
388
389 sub do_deploy(@) {
390     my $name = $_[0] || "";
391     if ($name ne "") {
392         my $data = "{\"XappName\":\"$name\"";
393         $data = append_option($data, "helmVersion");
394         $data = append_option($data, "releaseName");
395         $data = append_option($data, "namespace");
396         $data = append_option($data, "overrideFile");
397         $data = $data . "}";
398         make_temps();
399         if (rest("POST", $base_xapps, $data)) {
400             if ($http_code eq "201") {
401                 print_json $resultfile;
402                 $status = 0;
403             }
404             else {
405                 my $error;
406                 if ($http_code eq "400") {
407                     $error = "INVALID PARAMETERS SUPPLIED";
408                 }
409                 elsif ($http_code eq "500") {
410                     $error = "INTERNAL ERROR";
411                 }
412                 else {
413                     $error = "UNKNOWN STATUS $http_code";
414                 }
415                 print "$error\n";
416                 $status = 1;
417             }
418         }
419         else {
420             $status=1;
421         }
422         remove_temps();
423     }
424     else {
425         print "$myname: Error: expected the name of xapp to deploy\n";
426         $status = 1;
427     }
428 }
429
430 sub do_undeploy(@) {
431     my $name = $_[0] || "";
432     my $urlpath = $base_xapps;
433     if ($name ne "") {
434         make_temps();
435         $urlpath = "$urlpath/$name";
436         if (rest("DELETE", $urlpath)) {
437             if ($http_code eq "204") {
438                 print "SUCCESSFUL DELETION\n";
439                 $status = 0;
440             }
441             else {
442                 my $error;
443                 if ($http_code eq "400") {
444                     $error = "INVALID XAPP NAME SUPPLIED";
445                 }
446                 elsif ($http_code eq "500") {
447                     $error = "INTERNAL ERROR";
448                 }
449                 else {
450                     $error = "UNKNOWN STATUS $http_code";
451                 }
452                 print "$error\n";
453                 $status = 1;
454             }
455         }
456         else {
457             $status = 1;
458         }
459         remove_temps();
460     }
461     else {
462         print "$myname: Error: expected the name of xapp to undeploy\n";
463         $status = 1;
464     }
465 }
466
467 sub do_status(@) {
468     my $name = $_[0] || "";
469     my $instance = $_[1] || "";
470     my $urlpath = $base_xapps;
471
472     if ($name ne "") {
473         $urlpath = "$urlpath/$name";
474     }
475     if ($instance ne "") {
476         $urlpath = "$urlpath/instances/$instance"
477     }
478     make_temps();
479     if (rest("GET", $urlpath)) {
480         if ($http_code eq "200") {
481             print_json $resultfile;
482             $status = 0;
483         }
484         else {
485             my $error;
486             if ($http_code eq "400") {
487                 $error = "INVALID XAPP NAME SUPPLIED";
488             }
489             if ($http_code eq "404") {
490                 $error = "XAPP NOT FOUND";
491             }
492             elsif ($http_code eq "500") {
493                 $error = "INTERNAL ERROR";
494             }
495             else {
496                 $error = "UNKNOWN STATUS $http_code";
497             }
498             print "$error\n";
499             $status = 1;
500         }
501     }
502     else {
503         $status = 1;
504     }
505     remove_temps();
506 }
507
508 # Helpers for subscription:
509 # Validate the subscription data that follows a subscription add or modify
510 # subcommand. $1=URL, $2=eventType, $3=maxRetries, $4=retryTimer
511 # URL must look like URL, event type must be one of created deleted all,
512 # maxRetries and retryTimer must be non-negative numbers.
513 # If errors, returns false (0) and prints errors, else returns 1.
514 #
515 sub validate_subscription(@) {
516     # Using the API parameter names
517     my $targetUrl = $_[0] || "";
518     my $eventType = $_[1] || "";
519     my $maxRetries = $_[2] || "";
520     my $retryTimer = $_[3] || "";
521     my $retval = 1;
522
523     if (! ($targetUrl =~ /^http:\/\/.*/ or $targetUrl =~ /^https:\/\/.*/)) {
524         print "$myname: bad URL $targetUrl\n";
525         $retval = 0;
526     }
527     if ($eventType ne "created" and $eventType ne "deleted" and
528         $eventType ne "all") {
529         print "$myname: unrecognized event $eventType\n";
530         $retval = 0;
531     }
532     if (! ($maxRetries =~ /^[0-9]+$/)) {
533         print "$myname: invalid maximum retries count $maxRetries\n";
534         $retval = 0;
535     }
536     if (! ($retryTimer =~ /^[0-9]+$/)) {
537         print "$myname: invalid retry time $retryTimer\n";
538         $retval = 0;
539     }
540     return $retval;
541 }
542
543 # Format a subscriptionRequest JSON object
544
545 sub make_subscriptionRequest(@) {
546     my $targetUrl = $_[0];
547     my $eventType = $_[1];
548     my $maxRetries = $_[2];
549     my $retryTimer = $_[3];
550     return "{\"Data\": {\"TargetUrl\":\"$targetUrl\",\"EventType\":\"$eventType\",\"MaxRetries\":$maxRetries,\"RetryTimer\":$retryTimer}}";
551 }
552
553 # Subscriptions:
554 # $1 is sub-command: list, add, delete, modify
555
556 sub do_subscriptions(@) {
557     my $subcommand = $_[0] || "";
558     shift;
559
560     my %subcommands = (
561         "list" => \&do_subscription_list,
562         "add" => \&do_subscription_add,
563         "delete" => \&do_subscription_delete,
564         "del" => \&do_subscription_delete,
565         "modify" => \&do_subscription_modify,
566         "mod" => \&do_subscription_modify
567     );
568     if (exists $subcommands{$subcommand}) {
569         $subcommands{$subcommand}(@_);
570     }
571     else {
572         print "$myname: unrecognized subscriptions subcommand $subcommand\n";
573         helphint();
574         $status=1
575     }
576 }
577
578 # list: With empty parameter, list all, else the parameter is
579 # a subscriptionId
580
581 sub do_subscription_list(@) {
582     my $urlpath=$base_subs;
583     my $subscriptionId = $_[0] || "";
584     if ($subscriptionId ne "") {
585         $urlpath = "$urlpath/$subscriptionId";
586     }
587     make_temps();
588     if (rest("GET", $urlpath)) {
589         if ($http_code eq "200") {
590             print_json $resultfile;
591             $status = 0;
592         }
593         else {
594             my $error;
595             if ($http_code eq "400") {
596                 $error = "INVALID SUBSCRIPTION ID $subscriptionId";
597             }
598             elsif ($http_code eq "404") {
599                 $error = "SUBSCRIPTION $subscriptionId NOT FOUND";
600             }
601             elsif ($http_code eq "500") {
602                 $error = "INTERNAL ERROR";
603             }
604             else {
605                 $error = "UNKNOWN STATUS $http_code";
606             }
607             print "$error\n";
608             $status = 1;
609         }
610     }
611     else {
612         $status=1;
613     }
614     remove_temps();
615 }
616
617 sub do_subscription_add(@) {
618     my $urlpath=$base_subs;
619
620     if (validate_subscription(@_)) {
621         make_temps();
622         if (rest("POST", $urlpath, make_subscriptionRequest(@_))) {
623             if ($http_code eq "201") {
624                 print_json $resultfile;
625                 $status = 0;
626             }
627             else {
628                 my $error;
629                 if ($http_code eq "400") {
630                     $error = "INVALID INPUT";
631                 }
632                 elsif ($http_code eq "500") {
633                     $error = "INTERNAL ERROR";
634                 }
635                 else {
636                     $error = "UNKNOWN STATUS $http_code";
637                 }
638                 print "$error\n";
639                 $status = 1;
640             }
641         }
642         else {
643             $status=1;
644         }
645         remove_temps();
646     }
647     else {
648         $status = 1;
649     }
650 }
651
652 sub do_subscription_delete(@) {
653     my $urlpath=$base_subs;
654     my $subscriptionId = $_[0] || "";
655     if ($subscriptionId ne "") {
656         $urlpath = "$urlpath/$subscriptionId";
657     }
658     else {
659         print "$myname: delete: Subscription id required\n";
660         $status=1;
661         return;
662     }
663     make_temps();
664     if (rest("DELETE", $urlpath)) {
665         if ($http_code eq "204") {
666             print "SUBSCRIPTION $subscriptionId DELETED\n";
667             $status = 0;
668         }
669         else {
670             my $error;
671             if ($http_code eq "400") {
672                 $error = "INVALID SUBSCRIPTION ID $subscriptionId";
673             }
674             elsif ($http_code eq "500") {
675                 $error = "INTERNAL ERROR";
676             }
677             else {
678                 $error = "UNKNOWN STATUS $http_code";
679             }
680             print "$error\n";
681             $status = 1;
682         }
683     }
684     else {
685         $status = 1;
686     }
687     remove_temps();
688 }
689
690 sub do_subscription_modify(@) {
691     my $urlpath=$base_subs;
692     if (defined $_[0]) {
693         $urlpath = "$urlpath/$_[0]";
694     }
695     else {
696         print "$myname: modify: Subscription id required\n";
697         $status=1;
698         return;
699     }
700     shift;
701     if (validate_subscription(@_)) {
702         make_temps();
703         if (rest("PUT", $urlpath, make_subscriptionRequest(@_))) {
704             if ($http_code eq "200") {
705                 print_json $resultfile;
706                 $status = 0;
707             }
708             else {
709                 my $error;
710                 if ($http_code eq "400") {
711                     $error = "INVALID INPUT";
712                 }
713                 elsif ($http_code eq "500") {
714                     $error = "INTERNAL ERROR";
715                 }
716                 else {
717                     $error = "UNKNOWN STATUS $http_code";
718                 }
719                 print "$error\n";
720                 $status = 1;
721             }
722         }
723         else {
724             $status=1;
725         }
726         remove_temps();
727     }
728     else {
729         $status = 1;
730     }
731 }
732
733 sub do_health(@) {
734     my $urlpath=$base_health;
735     my $check = $_[0] || "";
736     # API now defines two types of checks, either of
737     # which must be specified.
738     if ($check ne "alive" and $check ne "ready") {
739         print "$myname: health check type required (alive or ready)\n";
740         $status=1;
741         return;
742     }
743     $urlpath = "$urlpath/$check";
744     make_temps();
745     if (rest("GET", $urlpath)) {
746         my $res;
747         if ($check eq "alive") {
748             # If GET succeeds at all, the xapp manager is alive, no
749             # need to check the HTTP code.
750             $res = "ALIVE";
751         }
752         else {
753             if ($http_code eq "200") {
754                 $res = "READY";
755             }
756             elsif ($http_code eq "503") {
757                 $res = "NOT READY";
758             }
759             elsif ($http_code eq "500") {
760                 $res = "INTERNAL ERROR";
761             }
762             else {
763                 $res = "UNKNOWN STATUS $http_code";
764             }
765         }
766         print "$res\n";
767     }
768     else {
769         $status = 1;
770         print "$myname: health check failed to contact appmgr\n";
771     }
772     remove_temps();
773 }
774
775 sub do_config(@) {
776     my $subcommand = $_[0] || "";
777     shift;
778
779     my %subcommands = (
780         "list" => \&do_config_list,
781         "add" => \&do_config_add,
782         "delete" => \&do_config_delete,
783         "del" => \&do_config_delete,
784         "modify" => \&do_config_modify,
785         "mod" => \&do_config_modify
786     );
787     if (exists $subcommands{$subcommand}) {
788         $subcommands{$subcommand}(@_);
789     }
790     else {
791         print "$myname: unrecognized config subcommand $subcommand\n";
792         helphint();
793         $status=1
794     }
795 }
796
797 sub do_config_list(@) {
798     if (defined $_[0]) {
799         print "$myname: \"config list\" has no parameters\n";
800         $status = 1;
801         return;
802     }
803     make_temps();
804     if (rest("GET", $base_config)) {
805         if ($http_code eq "200") {
806             print_json $resultfile;
807             $status = 0;
808         }
809         else {
810             my $error;
811             if ($http_code eq "500") {
812                 $error = "INTERNAL ERROR";
813             }
814             else {
815                 $error = "UNKNOWN STATUS $http_code";
816             }
817             print "$error\n";
818             $status = 1;
819         }
820     }
821     else {
822         $status=1;
823     }
824     remove_temps();
825 }
826
827 # validate_config() checks configuration commmand line.
828 # "config add" and "config modify" expect either single parameter which
829 # must be a JSON file that contains the whole thing to send (see API),
830 # or 5 parameters, where the first three are
831 # $_[0] = name
832 # $_[1] = configName (name of the configMap)
833 # $_[2] = namespace
834 # Followed by two file names:
835 # $_[3] = file containing configSchema
836 # $_[4] = file containing data for configMap
837 # Giving the last two literally on the command line does not make much sense,
838 # since they are arbitrary JSON data.
839 # On success, returns parameter count (1 or 5), depending on which kind of
840 # command line found.
841 # 0 if errors.
842
843 # Check only the 3 names at the beginning of config add/modify/delete
844 sub validate_config_names(@) {
845     my $retval = 1;
846     # Names in the Kubernetes world consist of lowercase alphanumerics
847     # and - and . as specified in
848     # https://kubernetes.io/docs/concepts/overview/working-with-objects/name
849     for (my $idx = 0; $idx <= 2; ++$idx) {
850         if (! ($_[$idx] =~ /^[a-z][-a-z0-9.]*$/)) {
851             print "$myname: invalid characters in name $_[$idx]\n";
852             $retval = 0;
853         }
854     }
855     return $retval;
856 }
857
858 sub validate_config(@) {
859     my $retval = 1;
860     print "validate_config args @_\n";
861     if ($#_ == 0) {
862         if (! -r $_[0]) {
863             print "$myname: config file $_[0] cannot be read: $!\n";
864             $retval = 0;
865         }
866     }
867     elsif ($#_ == 4) {
868         $retval = 5;
869         if (! validate_config_names(@_)) {
870             $retval = 0;
871         }
872         for (my $idx = 3; $idx <= 4; ++$idx) {
873             if (! -r $_[$idx]) {
874                 print "$myname: cannot read file $_[$idx]\n";
875                 $retval = 0;
876             }
877         }
878     }
879     else {
880         print "$myname: config add: 1 or 5 parameter expected\n";
881         $retval = 0;
882     }
883     return $retval;
884 }
885
886 # Generate JSON for the xAppConfig element (see API).
887
888 sub make_xAppConfigInfo($$$) {
889     return "{\"xAppName\":\"$_[0]\",\"configMapName\":\"$_[1]\",\"namespace\":\"$_[2]\"}";
890 }
891
892 sub make_xAppConfig(@) {
893     my $retval =  "{\"xAppConfigInfo\":" . make_xAppConfigInfo($_[0],$_[1],$_[2]);
894     my $fh;
895     open($fh, "<", $_[3]) or die "failed to open $_[3]";
896     my @obj = <$fh>;
897     close($fh);
898     $retval = $retval . ",\"configSchema\":" . join("", @obj);
899     open($fh, "<", $_[4]) or die "failed to open $_[4]";
900     @obj = <$fh>;
901     close($fh);
902     $retval = $retval . ",\"configMap\":" . join("", @obj) . "}";
903 }
904
905 sub do_config_add(@) {
906     my $paramCount;
907
908     $paramCount = validate_config(@_);
909     if ($paramCount > 0) {
910         my $xAppConfig;
911         if ($paramCount == 1) {
912             $xAppConfig = "\@$_[0]";
913         }
914         else {
915             $xAppConfig = make_xAppConfig(@_);
916         }
917         make_temps();
918         if (rest("POST", $base_config, $xAppConfig)) {
919             if ($http_code eq "201") {
920                 print_json $resultfile;
921                 $status = 0;
922             }
923             elsif ($http_code eq "422") { # Validation failed, details in result
924                 print_json $resultfile;
925                 $status = 1;
926             }
927             else {
928                 my $error;
929                 if ($http_code eq "400") {
930                     $error = "INVALID INPUT";
931                 }
932                 elsif ($http_code eq "500") {
933                     $error = "INTERNAL ERROR";
934                 }
935                 else {
936                     $error = "UNKNOWN STATUS $http_code";
937                 }
938                 print "$error\n";
939                 $status = 1;
940             }
941         }
942         else {
943             $status=1;
944         }
945         remove_temps();
946     }
947     else {
948         $status = 1;
949     }
950 }
951
952 sub do_config_modify(@) {
953     my $paramCount;
954
955     $paramCount = validate_config(@_);
956     if ($paramCount > 0) {
957         my $xAppConfig;
958         if ($paramCount == 1) {
959             $xAppConfig = "\@$_[0]";
960         }
961         else {
962             $xAppConfig = make_xAppConfig(@_);
963         }
964         make_temps();
965         if (rest("PUT", $base_config, $xAppConfig)) {
966             if ($http_code eq "200") {
967                 print_json $resultfile;
968                 $status = 0;
969             }
970             elsif ($http_code eq "422") { # Validation failed, details in result
971                 print_json $resultfile;
972                 $status = 1;
973             }
974             else {
975                 my $error;
976                 if ($http_code eq "400") {
977                     $error = "INVALID INPUT";
978                 }
979                 elsif ($http_code eq "500") {
980                     $error = "INTERNAL ERROR";
981                 }
982                 else {
983                     $error = "UNKNOWN STATUS $http_code";
984                 }
985                 print "$error\n";
986                 $status = 1;
987             }
988         }
989         else {
990             $status=1;
991         }
992         remove_temps();
993     }
994     else {
995         $status = 1;
996     }
997 }
998
999 # In config delete, allow either 1 parameter naming a file that contains
1000 # a JSON xAppConfigInfo object, or 3 parameters giving the
1001 # components (xAppName, configMapName, namespace), same as
1002 # in add and modify operations.
1003
1004 sub do_config_delete(@) {
1005     my $xAppConfigInfo = "";
1006
1007     if ($#_ != 0 and $#_ != 2) {
1008         print "$myname: wrong number of parameters for config delete\n";
1009         $status = 1;
1010     }
1011     elsif ($#_ == 0) {
1012         if (-r $_[0]) {
1013             $xAppConfigInfo = "\@$_[0]";
1014         }
1015         else {
1016             print "$myname: config file $_[0] cannot be read: $!\n";
1017             $status = 1;
1018         }
1019     }
1020     elsif (($#_ == 2) && validate_config_names(@_)) {
1021         $xAppConfigInfo = make_xAppConfigInfo($_[0],$_[1],$_[2]);
1022     }
1023     else {
1024         print "$myname: bad parameters for config delete\n";
1025         $status = 1;
1026     }
1027     if ($xAppConfigInfo ne "") {
1028         make_temps();
1029         if (rest("DELETE", $base_config, $xAppConfigInfo)) {
1030             if ($http_code eq "204") {
1031                 print "SUCCESFUL DELETION OF CONFIG\n";
1032                 $status = 0;
1033             }
1034             else {
1035                 my $error;
1036                 if ($http_code eq "400") {
1037                     $error = "INVALID PARAMETERS SUPPLIED";
1038                 }
1039                 elsif ($http_code eq "500") {
1040                     $error = "INTERNAL ERROR";
1041                 }
1042                 else {
1043                     $error = "UNKNOWN STATUS $http_code";
1044                 }
1045                 print "$error\n";
1046                 $status = 1;
1047             }
1048         }
1049         else {
1050             $status=1;
1051         }
1052         remove_temps();
1053     }
1054 }