dsci-runner.git | common/ | sparrowfile


#!raku

use Sparky::JobApi;

use JSON::Fast;

use YAMLish;

class Pipeline

does Sparky::JobApi::Role

{

  method !version {
      return "0.1.0"
  }

  method !validate-cond-expression ($exp,$debug = False) {
    my @b; # bogus lines
    for $exp.split(/\s [ "and" || "or" || "&&" || "||" ]  \s/)
    .map({
      .subst("(","",:g)
      .subst(")","",:g)
      .subst(/^^ \s+/,"")
      .subst(/\s+ $$/,"")
      .subst('$$',"rx_end_of_line",:g)
      }) -> $i {
      say "parse [$i]" if $debug;  
      if  $i !~~ /
          ^^ 
            ".<" 
              \w+ 
            ">" 
          \s+ 
          [ 
            ">" || ">=" || "<" || "<=" || "==" || "!=" || "eq" || "ne" ||  "~~" || "!~~" 
          ]
          \s+
          [
            \" <[ \/ \\ \d \. \: \w \d ]> + \" || 
            \d+ || 
            "m:" <[i m g r s \:]> + \s* \/ <-[ \{ \{ \$  ]> +  \/ ||
            \/ <-[ \{ \{ \$  ]> + \/
          ] 
          $$
        / {
        push @b, $i
      }
    }
    if @b.elems {
      say "validate-cond-expression: does not look good";
      say "validated expression: $exp";
      say "===";
      for @b -> $i {
        say "bogus chunk found: $i"
      }
      say "===";
    }
    return @b.elems == 0;
  }

  method stage-main {

    directory "scm";

    my $cr = tags()<cr> || "docker"; # container runtime: docker|podman

    say "scm: ",tags()<scm>;
    say "sha: ",tags()<sha>;
    say "message: ",tags()<message>.chomp;
    say "container runtime: ", $cr;
    say "runner version: {self!version()}";
    say "=======";

    bash "git config --global advice.detachedHead false";

    my $scm = tags()<scm>;

    git-scm tags()<scm>, %(
      :to<scm>,
      :branch(tags()<sha>),
      :skip-pull,
    );

    my $jobs = load-yaml("scm/.dsci/jobs.yaml".IO.slurp);

    my $status = True;

    #say $jobs.raku;

    my $cont = True;

    my $localhost-mode = $jobs<global> && $jobs<global><localhost>;

    say "pipeline mode: {$localhost-mode ?? 'localhost' !! 'docker'}";

    if $localhost-mode and ! tags()<DsciAllowLocalhostModeRepos> {
      die "localhost is forbidden, DsciAllowLocalhostModeRepos is not set";
      #$status = False;
      #$cont = False; 
    } elsif $localhost-mode and tags()<DsciAllowLocalhostModeRepos> {
      my @repos = tags()<DsciAllowLocalhostModeRepos>.split(":");
      $status = False;
      for @repos -> $i {
        if tags()<repo_full_name> eq $i {
          $status = True;
          last();
        }
      }
      unless $status {
        die "localhost is forbidden, {tags()<repo_full_name>} should be listed in DsciAllowLocalhostModeRepos";
        #$cont = False;
      }
    }

    if $cont && $jobs<global> && $jobs<global><skip> {
      if self!validate-cond-expression($jobs<global><skip>,$jobs<global><debug>||False) {
        use MONKEY-SEE-NO-EVAL;
        $cont = False if EVAL("my \$a = \{given (tags()) \{ {$jobs<global><skip>} \}\}; \$a()");
      } else {
        warn "skip: job condition validation failed";
        $status = False;
        $cont = False; 
      }
    }

    if $cont && $jobs<global> && $jobs<global><only> {
      if self!validate-cond-expression($jobs<global><only>,$jobs<global><debug>||False) {
        use MONKEY-SEE-NO-EVAL;
        $cont = False unless EVAL("my \$a = \{given (tags()) \{ {$jobs<global><only>} \}\}; \$a()");
      } else {
        warn "only: job condition validation failed";
        $status = False;
        $cont = False; 
      }
    }

    if $cont {

      my %_dsci;
    
      if $localhost-mode {

        unless tags()<DsciAgentSkipBootstrap> {
          my $j = Sparky::JobApi.new(
              :project("job.bootstrap"),
          );

          $j.queue({
              :description("dsci bootstrap"),
              :tags(%(stage => "bootstrap", SPARKY_HOST => "127.0.0.1")),
              :sparrowdo(%(
                :host<127.0.0.1>,
                :ssh_user(%*ENV<HOST_SSH_USER>),
                :bootstrap,
                :no_sudo,
              )),
          });

          my $st = self.wait-job($j);

          if ! $st<OK> {
            $status = False;
            my $eff-status = $st<TIMEOUT> ?? "TIMEOUT" !! "FAIL";
            say "dsci bootstrap ... [$eff-status]";
            say "...";
            say $j.report();
          } else {
            say "dsci bootstrap ... [OK]";
            say "...";
            say $j.report();
          }

        }

      } else {

        if $cr eq "docker" {
          bash "sudo chmod 666 /var/run/docker.sock";
        }

        bash "$cr stop -t 1 dsci-agent || :";

        my @env;

        if "{%*ENV<HOME>}/.secrets/{tags()<repo_full_name>}".IO ~~ :d {
          for dir("{%*ENV<HOME>}/.secrets/{tags()<repo_full_name>}") -> $file {
            #say $file;
            next unless $file ~~ :f;
            push @env, "-e {$file.basename}=\"{$file.slurp.chomp}\"";
            say "push secret {$file.basename} to job container ...";
          }
        }

        say "run dsci agent container from image: {tags()<DsciAgentImage>||'alpine:latest'}";

        say "$cr run --network host  -dit --rm --name dsci-agent --entrypoint /bin/sh {tags()<DsciAgentImage> || 'alpine:latest'}";

        bash "$cr run --network host  -dit --rm --name dsci-agent --entrypoint /bin/sh {@env.join(' ')} {tags()<DsciAgentImage> || 'alpine:latest'}";

        unless tags()<DsciAgentSkipBootstrap> {
          my $j = Sparky::JobApi.new(
              :project("job.bootstrap"),
          );

          $j.queue({
              :description("dsci agent bootstrap"),
              :tags(%(stage => "bootstrap", SPARKY_HOST => "127.0.0.1")),
              :sparrowdo(%(
                :docker<dsci-agent>,
                :bootstrap,
                :no_sudo,
              )),
          });
    

          my $st = self.wait-job($j);

          if ! $st<OK> {
            $status = False;
            my $eff-status = $st<TIMEOUT> ?? "TIMEOUT" !! "FAIL";
            say "dsci agent bootstrap ... [$eff-status]";
            say "...";
            say $j.report();
          } else {
            say "dsci agent bootstrap ... [OK]";
            say "...";
            say $j.report();
          }

        }

      }

      if $status {

        for $jobs<jobs>.flat -> $job {

          if $job<only> {
            if self!validate-cond-expression($job<only>,$job<debug>||False) {
              use MONKEY-SEE-NO-EVAL;
              next unless EVAL("my \$a = \{given (tags()) \{ {$job<only>} \}\}; \$a()");
            } else {
              warn "only: job condition validation failed";
              $status = False;
              last; 
            }
          }

          if $job<skip> {
            if self!validate-cond-expression($job<skip>,$job<debug>||False) {
            use MONKEY-SEE-NO-EVAL;
              next if EVAL("my \$a = \{given (tags()) \{ {$job<skip>} \}\}; \$a()");
            } else {
              warn "skip: job condition validation failed";
              $status = False;
              last; 
            }
          }

          my $j = Sparky::JobApi.new(
            :project("job.run"),
          );

          my %tags = (
            job_id  => $job<id>,
            path    => $job<path>,
            stage   => "job-run",
            scm     => tags()<scm>,
            sha     => tags()<sha>,
            SPARKY_HOST => "127.0.0.1",
            ref     => tags()<ref>,
            repo_full_name  => tags()<repo_full_name>,
            message => tags()<message>,
            dispatcher_job_id => tags()<SPARKY_JOB_ID>,
          );

          if $job<plugin> {
            %tags<plugin> = $job<plugin>
          } elsif $job<path> {
            %tags<path> = $job<path>
          } else {
            $status = False;
            say "neither path nor path is set";
            last;
          }

          if $job<debug> {
            %tags<dump-task-config> = True;
            %tags<dump-task-code> = True;
          }

          # copy dsci internal cache
          # to job input parameters
          # will be available
          # via config()<dsci>
          # object

          if $job<params> {
            $j.put-stash(%(
              params => $job<params>,
              dsci => %_dsci,
            ));
          } else {
            $j.put-stash(%(
              dsci => %_dsci,
              params => Hash.new,
            ));
          }

          my %sparrowdo = %( :no_sudo );
          if $localhost-mode {
            $j.queue({
              :description($job<id>),
              :tags(%tags),
              sparrowdo => %(
                :no_sudo,
                :host<127.0.0.1>,
                :ssh_user("{%*ENV<HOST_SSH_USER>}"),
              ),
            });
          } else {
            $j.queue({
              :description($job<id>),
              :tags(%tags),
              sparrowdo => %( 
                :no_sudo, 
                :docker<dsci-agent>,
              ),
            });
          }

          my $st = self.wait-job($j);

          unless $st<OK> {
            $status = False;
            my $eff-status = $st<TIMEOUT> ?? "TIMEOUT" !! "FAIL";
            say "job {$job<id>} ... [$eff-status]";
            say "...";
            say $j.report();
            last;
          }

          my $job-data = $j.get-stash();

          # copy job state to dsci internal cache
          %_dsci{$job<id>} = $job-data;

          # copy job artifacts (if any) to dsci internal cache
          if $job-data<artifacts> {
            %_dsci<dsci_artifacts> = $job-data<artifacts>
          }

          say "job {$job<id>} ... [OK]";
          say "...";
          say $j.report();

        }
      }

      if $status == False {
        die "some child jobs failed"
      }
    }
  }

  method stage-bootstrap {

    package-install "git";

    #bash "zef install --/test https://github.com/melezhik/Sparrow6.git";

    package-install "python3";

    #bash "zef install --/test --force-install https://github.com/melezhik/sparky-job-api.git", %(
    #  description => "update sparky-job-api",
    #);

    say "dsci agent is ready!!!";

  }

  method stage-job-run {

    #say tags().raku;

    my $me = Sparky::JobApi.new( :mine );

    say "run job: {tags()<job_id>}";

    say "===";

    #say $me.get-stash().raku;

    my $artifacts = $me.get-stash()<dsci><dsci_artifacts>;

    my $job-id = tags()<dispatcher_job_id>;
    my $project = "dsci";
    my $dispatcher = Sparky::JobApi.new( :$job-id, :$project );

    mkdir "{%*ENV<HOME>}/artifacts";

    if $artifacts {
      say "load artifacts from previous jobs ...";
      for $artifacts<> -> $i {
        say "load $i ..."; 
        my $data = $dispatcher.get-file($i);
        $i.IO.spurt($data);
        move($i,"{%*ENV<HOME>}/artifacts/$i");
        say "{%*ENV<HOME>}/artifacts/$i OK"; 
      }
    }

    if tags()<plugin> {

      my $params = $me.get-stash()<params>;

      if tags()<dump-task-code> {
        %*ENV<SP6_DUMP_TASK_CODE> =  1;
      }

      if tags()<dump-task-config> {
        %*ENV<SP6_DUMP_TASK_CONFIG> =  1;
      }

      # inject git repo parameters
      
      $params<DSCI_COMMIT> = tags()<sha>;
      $params<DSCI_SCM> = tags()<scm>;
      $params<DSCI_JOB_ID> = tags()<job_id>;
      $params<DSCI_REF> = tags()<ref>;
      $params<DSCI_REPO_FULL_NAME> = tags()<repo_full_name>;
      $params<DSCI_MESSAGE> = tags()<message>;

      my $s = task-run "plugin: {tags()<plugin>}", tags()<plugin>, $params;

      $me.put-stash($s);

      return;

    } 

    directory "scm";

    git-scm tags()<scm>, %(
      :to<scm>,
      :branch(tags()<sha>),
      :skip-pull,
    );


    chdir "scm/.dsci/{tags()<path>}";

    bash q:to /BASH/, %( cwd => "{$*CWD}", description => "prepare dsci scripts" );
        find . -type f -name job.py  | raku -e  'for lines() -> $i { my $c = $i.IO.slurp; $c = "from sparrow6lib import *\n\n$c"; $i.IO.spurt($c) }' 
        find . -type f -name task.py  | raku -e  'for lines() -> $i { my $c = $i.IO.slurp; $c = "from sparrow6lib import *\n\n$c"; $i.IO.spurt($c) }' 
        find . -type f -name hook.py  | raku -e  'for lines() -> $i { my $c = $i.IO.slurp; $c = "from sparrow6lib import *\n\n$c"; $i.IO.spurt($c) }' 
    BASH

    my %p_ = $me.get-stash();

    my %params = %p_<params>; 

    %params<_dsci_> = %p_<dsci>;

    if tags()<dump-task-config> {
      %*ENV<SP6_DUMP_TASK_CONFIG> =  1;
    }

    if tags()<dump-task-code> {
      %*ENV<SP6_DUMP_TASK_CODE> =  1;
    }

    # inject git repo parameters
      
    %params<DSCI_COMMIT> = tags()<sha>;
    %params<DSCI_SCM> = tags()<scm>;
    %params<DSCI_JOB_ID> = tags()<job_id>;
    %params<DSCI_REF> = tags()<ref>;
    %params<DSCI_REPO_FULL_NAME> = tags()<repo_full_name>;
    %params<DSCI_MESSAGE> = tags()<message>;

    my $s = task-run ".", %params;

    if %*ENV<HOME> && %*ENV<HOME>.IO ~~ :d  && "{%*ENV<HOME>}/artifacts".IO ~~ :d {
      say "saving job artifacts ...";
      $s<artifacts> = [];
      for dir("{%*ENV<HOME>}/artifacts") -> $i {
        if $i.IO ~~ :f {
          $dispatcher.put-file($i.Str,$i.IO.basename);
          push $s<artifacts>, $i.IO.basename;
        }
      }
    }

    $me.put-stash($s);

  }

}

Pipeline.new.run;