diff --git a/.github/workflows/smoke-test-s390x.yaml b/.github/workflows/smoke-test-s390x.yaml new file mode 100644 index 000000000..41495913e --- /dev/null +++ b/.github/workflows/smoke-test-s390x.yaml @@ -0,0 +1,11 @@ +name: s390x Runner Smoke Test +on: workflow_dispatch +jobs: + smoke-test: + runs-on: [self-hosted, S390X] + steps: + - name: Check architecture + run: | + echo "Architecture: $(uname -m)" + cat /etc/os-release | grep PRETTY_NAME + echo "Runner is alive on $(arch)" diff --git a/cmd/mapt/cmd/ibmcloud/hosts/ibm-power.go b/cmd/mapt/cmd/ibmcloud/hosts/ibm-power.go index b62855dec..b1a743826 100644 --- a/cmd/mapt/cmd/ibmcloud/hosts/ibm-power.go +++ b/cmd/mapt/cmd/ibmcloud/hosts/ibm-power.go @@ -2,6 +2,7 @@ package hosts import ( "github.com/redhat-developer/mapt/cmd/mapt/cmd/params" + "github.com/redhat-developer/mapt/pkg/integrations/github" "github.com/redhat-developer/mapt/pkg/integrations/gitlab" maptContext "github.com/redhat-developer/mapt/pkg/manager/context" ibmpower "github.com/redhat-developer/mapt/pkg/provider/ibmcloud/action/ibm-power" @@ -43,6 +44,10 @@ func ibmPowerCreate() *cobra.Command { if err := viper.BindPFlags(cmd.Flags()); err != nil { return err } + ghRunnerArgs := params.GithubRunnerArgs() + if ghRunnerArgs != nil { + ghRunnerArgs.Arch = &github.Ppc64le + } return ibmpower.New( &maptContext.ContextArgs{ Context: cmd.Context(), @@ -52,7 +57,7 @@ func ibmPowerCreate() *cobra.Command { Debug: viper.IsSet(params.Debug), DebugLevel: viper.GetUint(params.DebugLevel), CirrusPWArgs: params.CirrusPersistentWorkerArgs(), - GHRunnerArgs: params.GithubRunnerArgs(), + GHRunnerArgs: ghRunnerArgs, GLRunnerArgs: params.GitLabRunnerArgs(&gitlab.Ppc64le), Tags: viper.GetStringMapString(params.Tags), }, diff --git a/cmd/mapt/cmd/ibmcloud/hosts/ibm-z.go b/cmd/mapt/cmd/ibmcloud/hosts/ibm-z.go index 31a0bebed..a2ba0be4c 100644 --- a/cmd/mapt/cmd/ibmcloud/hosts/ibm-z.go +++ b/cmd/mapt/cmd/ibmcloud/hosts/ibm-z.go @@ -2,6 +2,7 @@ package hosts import ( "github.com/redhat-developer/mapt/cmd/mapt/cmd/params" + "github.com/redhat-developer/mapt/pkg/integrations/github" "github.com/redhat-developer/mapt/pkg/integrations/gitlab" maptContext "github.com/redhat-developer/mapt/pkg/manager/context" ibmz "github.com/redhat-developer/mapt/pkg/provider/ibmcloud/action/ibm-z" @@ -43,6 +44,10 @@ func ibmZCreate() *cobra.Command { if err := viper.BindPFlags(cmd.Flags()); err != nil { return err } + ghRunnerArgs := params.GithubRunnerArgs() + if ghRunnerArgs != nil { + ghRunnerArgs.Arch = &github.S390x + } return ibmz.New( &maptContext.ContextArgs{ Context: cmd.Context(), @@ -52,7 +57,7 @@ func ibmZCreate() *cobra.Command { Debug: viper.IsSet(params.Debug), DebugLevel: viper.GetUint(params.DebugLevel), CirrusPWArgs: params.CirrusPersistentWorkerArgs(), - GHRunnerArgs: params.GithubRunnerArgs(), + GHRunnerArgs: ghRunnerArgs, GLRunnerArgs: params.GitLabRunnerArgs(&gitlab.S390x), Tags: viper.GetStringMapString(params.Tags), }, diff --git a/cmd/mapt/cmd/params/params.go b/cmd/mapt/cmd/params/params.go index f6583a661..b543762b0 100644 --- a/cmd/mapt/cmd/params/params.go +++ b/cmd/mapt/cmd/params/params.go @@ -1,6 +1,10 @@ package params import ( + "fmt" + "os" + "strings" + "github.com/redhat-developer/mapt/pkg/integrations/cirrus" "github.com/redhat-developer/mapt/pkg/integrations/github" "github.com/redhat-developer/mapt/pkg/integrations/gitlab" @@ -73,9 +77,14 @@ const ( CreateCmdName string = "create" DestroyCmdName string = "destroy" - ghActionsRunnerToken string = "ghactions-runner-token" - ghActionsRunnerRepo string = "ghactions-runner-repo" - ghActionsRunnerLabels string = "ghactions-runner-labels" + ghActionsRunnerToken string = "ghactions-runner-token" + ghActionsRunnerRepo string = "ghactions-runner-repo" + ghActionsRunnerLabels string = "ghactions-runner-labels" + ghActionsRunnerImageRepo string = "ghactions-runner-image-repo" + // TODO: once the RHEL script is merged to https://github.com/IBM/action-runner-image-pz, + // switch default from deekay2310 fork to IBM upstream. + ghActionsRunnerImageRepoDefault string = "https://github.com/deekay2310/action-runner-image-pz.git" + GHActionsRunnerImageRepoDesc string = "Git clone URL for the action-runner-image-pz repository, used to build the GitHub Actions runner from source on ppc64le/s390x (no official binaries exist for these architectures)" cirrusPWToken string = "it-cirrus-pw-token" cirrusPWTokenDesc string = "Add mapt target as a cirrus persistent worker. The value will hold a valid token to be used by cirrus cli to join the project." @@ -278,18 +287,54 @@ func AddGHActionsFlags(fs *pflag.FlagSet) { fs.StringP(ghActionsRunnerToken, "", "", GHActionsRunnerTokenDesc) fs.StringP(ghActionsRunnerRepo, "", "", GHActionsRunnerRepoDesc) fs.StringSlice(ghActionsRunnerLabels, nil, GHActionsRunnerLabelsDesc) + fs.StringP(ghActionsRunnerImageRepo, "", ghActionsRunnerImageRepoDefault, GHActionsRunnerImageRepoDesc) } func GithubRunnerArgs() *github.GithubRunnerArgs { - if viper.IsSet(ghActionsRunnerToken) { - return &github.GithubRunnerArgs{ - Token: viper.GetString(ghActionsRunnerToken), - RepoURL: viper.GetString(ghActionsRunnerRepo), - Labels: viper.GetStringSlice(ghActionsRunnerLabels), - Platform: &github.Linux, - Arch: linuxArchAsGithubActionsArch( - viper.GetString(LinuxArch)), + token := viper.GetString(ghActionsRunnerToken) + repoURL := viper.GetString(ghActionsRunnerRepo) + pat := os.Getenv("GITHUB_TOKEN") + + if token == "" && pat == "" { + return nil + } + + if token == "" && repoURL == "" { + logging.Error("--ghactions-runner-repo is required for GitHub Actions runner setup") + return nil + } + + if token == "" { + logging.Info("no --ghactions-runner-token provided, auto-generating from GITHUB_TOKEN") + var err error + token, err = github.GenerateRegistrationToken(pat, repoURL) + if err != nil { + logging.Errorf("failed to auto-generate runner registration token: %v", err) + return nil } + logging.Info("runner registration token generated successfully") + } + + imageRepo := viper.GetString(ghActionsRunnerImageRepo) + if imageRepo != "" { + if err := validateRunnerImageRepo(imageRepo); err != nil { + logging.Errorf("invalid --ghactions-runner-image-repo: %v", err) + return nil + } + } + return &github.GithubRunnerArgs{ + Token: token, + RepoURL: repoURL, + Labels: viper.GetStringSlice(ghActionsRunnerLabels), + Platform: &github.Linux, + Arch: linuxArchAsGithubActionsArch(viper.GetString(LinuxArch)), + RunnerImageRepo: imageRepo, + } +} + +func validateRunnerImageRepo(repo string) error { + if !strings.HasPrefix(repo, "https://") { + return fmt.Errorf("only HTTPS URLs are allowed, got: %s", repo) } return nil } @@ -359,6 +404,10 @@ func linuxArchAsGithubActionsArch(arch string) *github.Arch { switch arch { case "x86_64": return &github.Amd64 + case "ppc64le": + return &github.Ppc64le + case "s390x": + return &github.S390x } return &github.Arm64 } diff --git a/pkg/integrations/github/api.go b/pkg/integrations/github/api.go new file mode 100644 index 000000000..ff66d844c --- /dev/null +++ b/pkg/integrations/github/api.go @@ -0,0 +1,66 @@ +package github + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + +type registrationTokenResponse struct { + Token string `json:"token"` + ExpiresAt string `json:"expires_at"` +} + +// GenerateRegistrationToken calls the GitHub API to create a short-lived +// runner registration token for the given repository. +// pat is a Personal Access Token with repo admin scope. +// repoURL is in the form "owner/repo" or "https://github.com/owner/repo". +func GenerateRegistrationToken(pat, repoURL string) (string, error) { + ownerRepo := repoURL + ownerRepo = strings.TrimPrefix(ownerRepo, "https://github.com/") + ownerRepo = strings.TrimPrefix(ownerRepo, "http://github.com/") + ownerRepo = strings.TrimSuffix(ownerRepo, "/") + + parts := strings.Split(ownerRepo, "/") + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", fmt.Errorf("invalid repo format %q, expected owner/repo", repoURL) + } + + url := fmt.Sprintf("https://api.github.com/repos/%s/%s/actions/runners/registration-token", parts[0], parts[1]) + + req, err := http.NewRequest(http.MethodPost, url, nil) + if err != nil { + return "", fmt.Errorf("creating request: %w", err) + } + req.Header.Set("Authorization", "token "+pat) + req.Header.Set("Accept", "application/vnd.github+json") + req.Header.Set("X-GitHub-Api-Version", "2022-11-28") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", fmt.Errorf("calling GitHub API: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("reading response: %w", err) + } + + if resp.StatusCode != http.StatusCreated { + return "", fmt.Errorf("GitHub API returned %d: %s (ensure GITHUB_TOKEN has admin scope on the repo)", resp.StatusCode, string(body)) + } + + var tokenResp registrationTokenResponse + if err := json.Unmarshal(body, &tokenResp); err != nil { + return "", fmt.Errorf("parsing response: %w", err) + } + + if tokenResp.Token == "" { + return "", fmt.Errorf("empty token in GitHub API response") + } + + return tokenResp.Token, nil +} diff --git a/pkg/integrations/github/ghrunner.go b/pkg/integrations/github/ghrunner.go index ecb772b03..17a3b44f2 100644 --- a/pkg/integrations/github/ghrunner.go +++ b/pkg/integrations/github/ghrunner.go @@ -23,12 +23,23 @@ var snippetLinux []byte //go:embed snippet-windows.ps1 var snippetWindows []byte +//go:embed snippet-linux-ppc64le.sh +var snippetLinuxPpc64le []byte + +//go:embed snippet-linux-s390x.sh +var snippetLinuxS390x []byte + var snippets map[Platform][]byte = map[Platform][]byte{ Darwin: snippetDarwin, Linux: snippetLinux, Windows: snippetWindows, } +var archSnippets map[Arch][]byte = map[Arch][]byte{ + Ppc64le: snippetLinuxPpc64le, + S390x: snippetLinuxS390x, +} + var runnerArgs *GithubRunnerArgs func Init(args *GithubRunnerArgs) { @@ -40,17 +51,22 @@ func (args *GithubRunnerArgs) GetUserDataValues() *integrations.UserDataValues { return nil } return &integrations.UserDataValues{ - Name: args.Name, - Token: args.Token, - Labels: getLabels(), - RepoURL: args.RepoURL, - CliURL: downloadURL(), + Name: args.Name, + Token: args.Token, + Labels: getLabels(), + RepoURL: args.RepoURL, + CliURL: downloadURL(), + RunnerImageRepo: args.RunnerImageRepo, } } func (args *GithubRunnerArgs) GetSetupScriptTemplate() string { - templateConfig := string(snippets[*runnerArgs.Platform][:]) - return templateConfig + if *runnerArgs.Platform == Linux && runnerArgs.Arch != nil { + if archSnippet, ok := archSnippets[*runnerArgs.Arch]; ok { + return string(archSnippet[:]) + } + } + return string(snippets[*runnerArgs.Platform][:]) } func GetRunnerArgs() *GithubRunnerArgs { diff --git a/pkg/integrations/github/snippet-linux-ppc64le.sh b/pkg/integrations/github/snippet-linux-ppc64le.sh new file mode 100644 index 000000000..bd5da65ad --- /dev/null +++ b/pkg/integrations/github/snippet-linux-ppc64le.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +set -euo pipefail + +dnf install -y git-core + +# Background sshd monitor: logs status every 30s to help diagnose build breakage +( + LOG=/var/log/sshd-watchdog.log + while true; do + echo "--- $(date) ---" >> "$LOG" + systemctl is-active sshd >> "$LOG" 2>&1 + ss -tlnp | grep :22 >> "$LOG" 2>&1 + sshd -T >> /var/log/sshd-configtest.log 2>&1 || echo "sshd -T FAILED (exit $?)" >> "$LOG" + sleep 30 + done +) & +WATCHDOG_PID=$! + +git clone --depth=1 "{{ .RunnerImageRepo }}" /opt/action-runner-image-pz + +cd /opt/action-runner-image-pz +# Allow build to continue past flaky upstream test failures +bash -c '. scripts/vm.sh rhel 9 minimal --skip-snap-lxd' || true + +kill $WATCHDOG_PID 2>/dev/null || true + +echo "=== POST-BUILD SSHD DIAGNOSTICS ===" >> /var/log/sshd-watchdog.log +systemctl status sshd >> /var/log/sshd-watchdog.log 2>&1 +sshd -T >> /var/log/sshd-watchdog.log 2>&1 || echo "sshd -T FAILED" >> /var/log/sshd-watchdog.log +journalctl -u sshd --no-pager -n 50 >> /var/log/sshd-watchdog.log 2>&1 +ls -la /etc/ssh/ssh_host_* >> /var/log/sshd-watchdog.log 2>&1 +ls -la /usr/share/crypto-policies/ >> /var/log/sshd-watchdog.log 2>&1 +cat /etc/pam.d/system-auth >> /var/log/sshd-watchdog.log 2>&1 + +# The upstream configure-system.sh runs chmod -R 777 /usr/share which makes +# the sshd privilege separation directory world-writable. sshd refuses to +# start when /usr/share/empty.sshd is not owned by root or is world-writable. +chmod 755 /usr/share/empty.sshd 2>/dev/null || true +chown root:root /usr/share/empty.sshd 2>/dev/null || true +# Also fix PAM duplicates from configure-limits.sh +for f in /etc/pam.d/system-auth /etc/pam.d/password-auth; do + if [ -f "$f" ]; then + awk '!seen[$0]++' "$f" > "${f}.tmp" && mv "${f}.tmp" "$f" + fi +done +systemctl restart sshd 2>/dev/null || true + +echo "=== POST-REPAIR SSHD STATUS ===" >> /var/log/sshd-watchdog.log +systemctl status sshd >> /var/log/sshd-watchdog.log 2>&1 +ss -tlnp | grep :22 >> /var/log/sshd-watchdog.log 2>&1 + +# Upload diagnostics to COS so we can read them without SSH +python3 -c " +import hashlib, hmac, urllib.request, datetime, os, socket +key_id = os.environ.get('COS_KEY_ID', '') +secret = os.environ.get('COS_SECRET', '') +endpoint = os.environ.get('COS_ENDPOINT', '') +bucket = 'mapt-test-bucket-evidence' +hostname = socket.gethostname() +obj = 'debug/' + hostname + '-sshd-watchdog.log' +if key_id and secret and endpoint: + with open('/var/log/sshd-watchdog.log', 'rb') as f: + body = f.read() + now = datetime.datetime.utcnow() + date_stamp = now.strftime('%Y%m%d') + amz_date = now.strftime('%Y%m%dT%H%M%SZ') + region = 'us-south' + service = 's3' + host = endpoint.replace('https://','').replace('http://','') + canonical_uri = '/' + bucket + '/' + obj + payload_hash = hashlib.sha256(body).hexdigest() + canonical_headers = 'host:' + host + '\n' + 'x-amz-content-sha256:' + payload_hash + '\n' + 'x-amz-date:' + amz_date + '\n' + signed_headers = 'host;x-amz-content-sha256;x-amz-date' + canonical_request = 'PUT\n' + canonical_uri + '\n\n' + canonical_headers + '\n' + signed_headers + '\n' + payload_hash + algorithm = 'AWS4-HMAC-SHA256' + credential_scope = date_stamp + '/' + region + '/' + service + '/aws4_request' + string_to_sign = algorithm + '\n' + amz_date + '\n' + credential_scope + '\n' + hashlib.sha256(canonical_request.encode()).hexdigest() + def sign(key, msg): + return hmac.new(key, msg.encode(), hashlib.sha256).digest() + signing_key = sign(sign(sign(sign(('AWS4' + secret).encode(), date_stamp), region), service), 'aws4_request') + signature = hmac.new(signing_key, string_to_sign.encode(), hashlib.sha256).hexdigest() + auth = algorithm + ' Credential=' + key_id + '/' + credential_scope + ', SignedHeaders=' + signed_headers + ', Signature=' + signature + req = urllib.request.Request(endpoint + canonical_uri, data=body, method='PUT') + req.add_header('x-amz-date', amz_date) + req.add_header('x-amz-content-sha256', payload_hash) + req.add_header('Authorization', auth) + req.add_header('Content-Type', 'text/plain') + urllib.request.urlopen(req) + print('Uploaded diagnostics to COS: ' + obj) +else: + print('COS credentials not set, skipping upload') +" 2>&1 || echo "COS upload failed" + +if [ ! -f /opt/runner-cache/config.sh ]; then + echo "Runner binary not found after build — check build logs" >&2 + exit 1 +fi + +id -u runner &>/dev/null || useradd -m -s /bin/bash runner +chown -R runner:runner /opt/runner-cache + +sudo -u runner bash -c ' + cd /opt/runner-cache + + ./config.sh \ + --unattended \ + --disableupdate \ + --ephemeral \ + --name "{{ .Name }}" \ + --labels "{{ .Labels }}" \ + --url "{{ .RepoURL }}" \ + --token "{{ .Token }}" + + nohup ./run.sh > /tmp/gh-runner.log 2>&1 & +' diff --git a/pkg/integrations/github/snippet-linux-s390x.sh b/pkg/integrations/github/snippet-linux-s390x.sh new file mode 100644 index 000000000..22bd2982d --- /dev/null +++ b/pkg/integrations/github/snippet-linux-s390x.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +apt-get update -y && apt-get install -y software-properties-common + +git clone --depth=1 "{{ .RunnerImageRepo }}" /opt/action-runner-image-pz + +cd /opt/action-runner-image-pz +# Allow build to continue past flaky upstream test failures +bash -c '. scripts/vm.sh ubuntu 22.04 minimal --skip-snap-lxd' || true + +if [ ! -f /opt/runner-cache/config.sh ]; then + echo "Runner binary not found after build — check build logs" >&2 + exit 1 +fi + +id -u runner &>/dev/null || useradd -m -s /bin/bash runner +chown -R runner:runner /opt/runner-cache /opt/dotnet + +sudo -u runner bash -c ' + cd /opt/runner-cache + export DOTNET_ROOT=/opt/dotnet + export PATH=$PATH:$DOTNET_ROOT + + ./config.sh \ + --unattended \ + --disableupdate \ + --ephemeral \ + --name "{{ .Name }}" \ + --labels "{{ .Labels }}" \ + --url "{{ .RepoURL }}" \ + --token "{{ .Token }}" + + nohup ./run.sh > /tmp/gh-runner.log 2>&1 & +' diff --git a/pkg/integrations/github/types.go b/pkg/integrations/github/types.go index ccc8974d3..192d96c98 100644 --- a/pkg/integrations/github/types.go +++ b/pkg/integrations/github/types.go @@ -8,17 +8,20 @@ var ( Linux Platform = "linux" Darwin Platform = "osx" - Arm64 Arch = "arm64" - Amd64 Arch = "x64" - Arm Arch = "arm" + Arm64 Arch = "arm64" + Amd64 Arch = "x64" + Arm Arch = "arm" + Ppc64le Arch = "ppc64le" + S390x Arch = "s390x" ) type GithubRunnerArgs struct { - Token string - RepoURL string - Name string - Platform *Platform - Arch *Arch - Labels []string - User string + Token string + RepoURL string + Name string + Platform *Platform + Arch *Arch + Labels []string + User string + RunnerImageRepo string } diff --git a/pkg/integrations/integrations.go b/pkg/integrations/integrations.go index 7b59ceef7..b8d9394f5 100644 --- a/pkg/integrations/integrations.go +++ b/pkg/integrations/integrations.go @@ -6,16 +6,17 @@ import ( ) type UserDataValues struct { - CliURL string - User string - Name string - Token string - Labels string - Port string - RepoURL string - Executor string - Unsecure bool - Concurrent int + CliURL string + User string + Name string + Token string + Labels string + Port string + RepoURL string + Executor string + Unsecure bool + Concurrent int + RunnerImageRepo string } type IntegrationConfig interface { diff --git a/pkg/provider/ibmcloud/action/ibm-power/cloud-config b/pkg/provider/ibmcloud/action/ibm-power/cloud-config index 7088603ee..83982e7bd 100644 --- a/pkg/provider/ibmcloud/action/ibm-power/cloud-config +++ b/pkg/provider/ibmcloud/action/ibm-power/cloud-config @@ -80,6 +80,13 @@ write_files: content: | {{.GitLabRunnerScript}} {{- end}} +{{- if .GHActionsRunnerScript}} + - path: /opt/install-ghrunner.sh + permissions: '0700' + owner: root:root + content: | +{{.GHActionsRunnerScript}} +{{- end}} runcmd: - systemctl enable mount-data-home.service - dnf install -y git podman policycoreutils-python-utils @@ -98,3 +105,10 @@ runcmd: - mkdir -p /var/log/gitlab-runner - bash /opt/install-glrunner.sh {{- end}} +{{- if .GHActionsRunnerScript}} + - | + export COS_KEY_ID="{{ .COSAccessKeyID }}" + export COS_SECRET="{{ .COSSecretAccessKey }}" + export COS_ENDPOINT="{{ .COSEndpoint }}" + bash /opt/install-ghrunner.sh +{{- end}} diff --git a/pkg/provider/ibmcloud/action/ibm-power/ibm-power.go b/pkg/provider/ibmcloud/action/ibm-power/ibm-power.go index 0adde3b7c..ab2557560 100644 --- a/pkg/provider/ibmcloud/action/ibm-power/ibm-power.go +++ b/pkg/provider/ibmcloud/action/ibm-power/ibm-power.go @@ -4,6 +4,7 @@ import ( _ "embed" "encoding/base64" "fmt" + "os" "strings" "github.com/mapt-oss/pulumi-ibmcloud/sdk/go/ibmcloud" @@ -11,6 +12,7 @@ import ( "github.com/pulumi/pulumi/sdk/v3/go/auto" "github.com/pulumi/pulumi/sdk/v3/go/pulumi" "github.com/redhat-developer/mapt/pkg/integrations" + "github.com/redhat-developer/mapt/pkg/integrations/github" "github.com/redhat-developer/mapt/pkg/integrations/gitlab" "github.com/redhat-developer/mapt/pkg/integrations/otelcol" "github.com/redhat-developer/mapt/pkg/manager" @@ -29,9 +31,13 @@ import ( var CloudConfig []byte type userDataValues struct { - Gateway string - OtelColScript string - GitLabRunnerScript string + Gateway string + OtelColScript string + GitLabRunnerScript string + GHActionsRunnerScript string + COSAccessKeyID string + COSSecretAccessKey string + COSEndpoint string } const ( @@ -183,6 +189,15 @@ func (r *pwRequest) deploy(ctx *pulumi.Context) error { } hasOtel := otelSet == 3 + ghRunnerScript := "" + if ghRunnerArgs := github.GetRunnerArgs(); ghRunnerArgs != nil { + s, err := integrations.GetIntegrationSnippetAsCloudInitWritableFile(ghRunnerArgs, defaultUser) + if err != nil { + return err + } + ghRunnerScript = *s + } + var piUserDataInput pulumi.StringPtrInput glRunnerArgs := gitlab.GetRunnerArgs() if glRunnerArgs != nil { @@ -192,6 +207,7 @@ func (r *pwRequest) deploy(ctx *pulumi.Context) error { } gateway := subnetInfo.Gateway localArgs := *glRunnerArgs + localGHScript := ghRunnerScript piUserDataInput = authToken.ApplyT(func(token string) (*string, error) { localArgs.AuthToken = token glSnippet, err := integrations.GetIntegrationSnippetAsCloudInitWritableFile(&localArgs, defaultUser) @@ -202,7 +218,7 @@ func (r *pwRequest) deploy(ctx *pulumi.Context) error { if hasOtel { otelArgs = r.otelArgs(true) } - ud, err := piUserData(gateway, otelArgs, *glSnippet) + ud, err := piUserData(gateway, otelArgs, *glSnippet, localGHScript) if err != nil { return nil, err } @@ -213,7 +229,7 @@ func (r *pwRequest) deploy(ctx *pulumi.Context) error { if hasOtel { otelArgs = r.otelArgs(false) } - ud, err := piUserData(subnetInfo.Gateway, otelArgs, "") + ud, err := piUserData(subnetInfo.Gateway, otelArgs, "", ghRunnerScript) if err != nil { return fmt.Errorf("failed to render user data: %w", err) } @@ -455,7 +471,7 @@ func (r *pwRequest) otelArgs(monitorGitLabRunner bool) *otelcol.OtelcolArgs { // piUserData renders the cloud-config template and returns it base64-encoded // for use as PiUserData on a PowerVS instance. -func piUserData(gateway string, otelArgs *otelcol.OtelcolArgs, glRunnerScript string) (string, error) { +func piUserData(gateway string, otelArgs *otelcol.OtelcolArgs, glRunnerScript, ghRunnerScript string) (string, error) { otelScript := "" if otelArgs != nil { s, err := otelcol.GetSnippetAsCloudInitWritableFile(otelArgs) @@ -466,9 +482,13 @@ func piUserData(gateway string, otelArgs *otelcol.OtelcolArgs, glRunnerScript st } script, err := file.Template( userDataValues{ - Gateway: gateway, - OtelColScript: otelScript, - GitLabRunnerScript: glRunnerScript, + Gateway: gateway, + OtelColScript: otelScript, + GitLabRunnerScript: glRunnerScript, + GHActionsRunnerScript: ghRunnerScript, + COSAccessKeyID: os.Getenv("IBMCLOUD_COS_ACCESS_KEY_ID"), + COSSecretAccessKey: os.Getenv("IBMCLOUD_COS_SECRET_ACCESS_KEY"), + COSEndpoint: os.Getenv("IBMCLOUD_COS_ENDPOINT"), }, string(CloudConfig)) if err != nil { diff --git a/pkg/provider/ibmcloud/action/ibm-power/ibm-power_test.go b/pkg/provider/ibmcloud/action/ibm-power/ibm-power_test.go index 12fee0b4d..d877bf8ec 100644 --- a/pkg/provider/ibmcloud/action/ibm-power/ibm-power_test.go +++ b/pkg/provider/ibmcloud/action/ibm-power/ibm-power_test.go @@ -9,7 +9,7 @@ import ( ) func TestPiUserData_noRunner(t *testing.T) { - out, err := piUserData("10.0.0.1", nil, "") + out, err := piUserData("10.0.0.1", nil, "", "") if err != nil { t.Fatalf("piUserData returned error: %v", err) } @@ -31,7 +31,7 @@ func TestPiUserData_noRunner(t *testing.T) { func TestPiUserData_withRunner(t *testing.T) { script := " #!/bin/bash\n echo hello" - out, err := piUserData("10.0.0.1", nil, script) + out, err := piUserData("10.0.0.1", nil, script, "") if err != nil { t.Fatalf("piUserData returned error: %v", err) } @@ -63,7 +63,7 @@ func TestPiUserData_withOtelAndRunner(t *testing.T) { SecurePath: "/var/log/secure", MonitorGitLabRunner: true, } - out, err := piUserData("10.0.0.1", args, script) + out, err := piUserData("10.0.0.1", args, script, "") if err != nil { t.Fatalf("piUserData returned error: %v", err) } diff --git a/pkg/provider/ibmcloud/action/ibm-z/cloud-config b/pkg/provider/ibmcloud/action/ibm-z/cloud-config index a1546c5d2..5f0bf1588 100644 --- a/pkg/provider/ibmcloud/action/ibm-z/cloud-config +++ b/pkg/provider/ibmcloud/action/ibm-z/cloud-config @@ -1,5 +1,5 @@ #cloud-config -{{- if or .OtelColScript .GitLabRunnerScript}} +{{- if or .OtelColScript .GitLabRunnerScript .GHActionsRunnerScript}} write_files: {{- if .OtelColScript}} - path: /opt/install-otelcol.sh @@ -33,6 +33,13 @@ write_files: content: | {{.GitLabRunnerScript}} {{- end}} +{{- if .GHActionsRunnerScript}} + - path: /opt/install-ghrunner.sh + permissions: '0700' + owner: root:root + content: | +{{.GHActionsRunnerScript}} +{{- end}} {{- end}} runcmd: - apt-get update -y @@ -44,3 +51,6 @@ runcmd: - mkdir -p /var/log/gitlab-runner - bash /opt/install-glrunner.sh {{- end}} +{{- if .GHActionsRunnerScript}} + - bash /opt/install-ghrunner.sh +{{- end}} diff --git a/pkg/provider/ibmcloud/action/ibm-z/ibm-z.go b/pkg/provider/ibmcloud/action/ibm-z/ibm-z.go index 432baf915..1e1dda372 100644 --- a/pkg/provider/ibmcloud/action/ibm-z/ibm-z.go +++ b/pkg/provider/ibmcloud/action/ibm-z/ibm-z.go @@ -12,6 +12,7 @@ import ( "github.com/pulumi/pulumi/sdk/v3/go/auto" "github.com/pulumi/pulumi/sdk/v3/go/pulumi" "github.com/redhat-developer/mapt/pkg/integrations" + "github.com/redhat-developer/mapt/pkg/integrations/github" "github.com/redhat-developer/mapt/pkg/integrations/gitlab" "github.com/redhat-developer/mapt/pkg/integrations/otelcol" "github.com/redhat-developer/mapt/pkg/manager" @@ -31,8 +32,9 @@ import ( var CloudConfig []byte type userDataValues struct { - OtelColScript string - GitLabRunnerScript string + OtelColScript string + GitLabRunnerScript string + GHActionsRunnerScript string } const ( @@ -360,8 +362,19 @@ func (r *zRequest) buildUserDataInput() (pulumi.StringPtrInput, error) { return nil, fmt.Errorf("partial otel configuration: --otel-app-code, --otel-auth-token, and --otel-index must all be set together") } hasOtel := otelSet == 3 + + ghRunnerScript := "" + if ghRunnerArgs := github.GetRunnerArgs(); ghRunnerArgs != nil { + s, err := integrations.GetIntegrationSnippetAsCloudInitWritableFile(ghRunnerArgs, defaultUser) + if err != nil { + return nil, err + } + ghRunnerScript = *s + } + if r.glAuthToken != nil { localArgs := *r.glRunnerArgsCopy + localGHScript := ghRunnerScript return r.glAuthToken.ApplyT(func(token string) (*string, error) { localArgs.AuthToken = token glSnippet, err := integrations.GetIntegrationSnippetAsCloudInitWritableFile(&localArgs, defaultUser) @@ -372,15 +385,19 @@ func (r *zRequest) buildUserDataInput() (pulumi.StringPtrInput, error) { if hasOtel { otelArgs = r.otelArgs(true) } - ud, err := izUserData(otelArgs, *glSnippet) + ud, err := izUserData(otelArgs, *glSnippet, localGHScript) if err != nil { return nil, err } return &ud, nil }).(pulumi.StringPtrOutput), nil } - if hasOtel { - ud, err := izUserData(r.otelArgs(false), "") + if hasOtel || ghRunnerScript != "" { + var otelArgs *otelcol.OtelcolArgs + if hasOtel { + otelArgs = r.otelArgs(false) + } + ud, err := izUserData(otelArgs, "", ghRunnerScript) if err != nil { return nil, fmt.Errorf("failed to render user data: %w", err) } @@ -403,7 +420,7 @@ func (r *zRequest) otelArgs(monitorGitLabRunner bool) *otelcol.OtelcolArgs { } } -func izUserData(otelArgs *otelcol.OtelcolArgs, glRunnerScript string) (string, error) { +func izUserData(otelArgs *otelcol.OtelcolArgs, glRunnerScript, ghRunnerScript string) (string, error) { otelScript := "" if otelArgs != nil { s, err := otelcol.GetSnippetAsCloudInitWritableFile(otelArgs) @@ -414,8 +431,9 @@ func izUserData(otelArgs *otelcol.OtelcolArgs, glRunnerScript string) (string, e } script, err := file.Template( userDataValues{ - OtelColScript: otelScript, - GitLabRunnerScript: glRunnerScript, + OtelColScript: otelScript, + GitLabRunnerScript: glRunnerScript, + GHActionsRunnerScript: ghRunnerScript, }, string(CloudConfig)) if err != nil { diff --git a/pkg/provider/ibmcloud/action/ibm-z/ibm-z_test.go b/pkg/provider/ibmcloud/action/ibm-z/ibm-z_test.go index b952fdebc..c2b84be7f 100644 --- a/pkg/provider/ibmcloud/action/ibm-z/ibm-z_test.go +++ b/pkg/provider/ibmcloud/action/ibm-z/ibm-z_test.go @@ -38,7 +38,7 @@ func decodeIzOutput(t *testing.T, out string) string { } func TestIzUserData_noRunner(t *testing.T) { - out, err := izUserData(nil, "") + out, err := izUserData(nil, "", "") if err != nil { t.Fatalf("izUserData returned error: %v", err) } @@ -56,7 +56,7 @@ func TestIzUserData_noRunner(t *testing.T) { func TestIzUserData_withRunner(t *testing.T) { script := " #!/bin/bash\n echo hello" - out, err := izUserData(nil, script) + out, err := izUserData(nil, script, "") if err != nil { t.Fatalf("izUserData returned error: %v", err) } @@ -81,7 +81,7 @@ func TestIzUserData_withOtelAndRunner(t *testing.T) { SecurePath: "/var/log/auth.log", MonitorGitLabRunner: true, } - out, err := izUserData(args, script) + out, err := izUserData(args, script, "") if err != nil { t.Fatalf("izUserData returned error: %v", err) }