From c9a4b05345fde898515880c6078a4c9816235df1 Mon Sep 17 00:00:00 2001 From: Dev Kumar Date: Tue, 9 Jun 2026 13:14:35 -0400 Subject: [PATCH 01/13] feat(ibmcloud): add GitHub Actions runner support for IBM Power and IBM Z --- cmd/mapt/cmd/ibmcloud/hosts/ibm-power.go | 7 +++- cmd/mapt/cmd/ibmcloud/hosts/ibm-z.go | 7 +++- cmd/mapt/cmd/params/params.go | 28 ++++++++++----- pkg/integrations/github/ghrunner.go | 30 ++++++++++++---- .../github/snippet-linux-ppc64le.sh | 22 ++++++++++++ .../github/snippet-linux-s390x.sh | 22 ++++++++++++ pkg/integrations/github/types.go | 23 +++++++------ pkg/integrations/integrations.go | 21 ++++++------ .../ibmcloud/action/ibm-power/cloud-config | 10 ++++++ .../ibmcloud/action/ibm-power/ibm-power.go | 31 ++++++++++++----- .../action/ibm-power/ibm-power_test.go | 6 ++-- .../ibmcloud/action/ibm-z/cloud-config | 12 ++++++- pkg/provider/ibmcloud/action/ibm-z/ibm-z.go | 34 ++++++++++++++----- .../ibmcloud/action/ibm-z/ibm-z_test.go | 6 ++-- 14 files changed, 197 insertions(+), 62 deletions(-) create mode 100644 pkg/integrations/github/snippet-linux-ppc64le.sh create mode 100644 pkg/integrations/github/snippet-linux-s390x.sh 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..bf8caead4 100644 --- a/cmd/mapt/cmd/params/params.go +++ b/cmd/mapt/cmd/params/params.go @@ -73,9 +73,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,17 +283,18 @@ 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), + Labels: viper.GetStringSlice(ghActionsRunnerLabels), + Platform: &github.Linux, + Arch: linuxArchAsGithubActionsArch(viper.GetString(LinuxArch)), + RunnerImageRepo: viper.GetString(ghActionsRunnerImageRepo), } } return nil @@ -359,6 +365,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/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..5c26bf1a1 --- /dev/null +++ b/pkg/integrations/github/snippet-linux-ppc64le.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +git clone {{ .RunnerImageRepo }} /opt/action-runner-image-pz + +cd /opt/action-runner-image-pz +bash -c '. scripts/vm.sh rhel 9 minimal --skip-snap-lxd' + +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 > /var/log/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..f11e43e1b --- /dev/null +++ b/pkg/integrations/github/snippet-linux-s390x.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +git clone {{ .RunnerImageRepo }} /opt/action-runner-image-pz + +cd /opt/action-runner-image-pz +bash -c '. scripts/vm.sh ubuntu 22.04 minimal --skip-snap-lxd' + +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 > /var/log/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..18efe36cd 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,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-power/ibm-power.go b/pkg/provider/ibmcloud/action/ibm-power/ibm-power.go index 0adde3b7c..a4618ae88 100644 --- a/pkg/provider/ibmcloud/action/ibm-power/ibm-power.go +++ b/pkg/provider/ibmcloud/action/ibm-power/ibm-power.go @@ -11,6 +11,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 +30,10 @@ import ( var CloudConfig []byte type userDataValues struct { - Gateway string - OtelColScript string - GitLabRunnerScript string + Gateway string + OtelColScript string + GitLabRunnerScript string + GHActionsRunnerScript string } const ( @@ -183,6 +185,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 +203,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 +214,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 +225,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 +467,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 +478,10 @@ 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, }, 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) } From 9af3e13553524ead564110558fce0c9f854707ad Mon Sep 17 00:00:00 2001 From: Dev Kumar Date: Tue, 9 Jun 2026 15:54:37 -0400 Subject: [PATCH 02/13] fix: harden --ghactions-runner-image-repo input - Quote the URL in snippet git clone commands to prevent shell injection - Add --depth=1 to limit clone exposure and speed up provisioning - Validate that only HTTPS URLs are accepted for the runner image repo Co-Authored-By: Claude Opus 4.6 --- cmd/mapt/cmd/params/params.go | 19 ++++++++++++++++++- .../github/snippet-linux-ppc64le.sh | 2 +- .../github/snippet-linux-s390x.sh | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/cmd/mapt/cmd/params/params.go b/cmd/mapt/cmd/params/params.go index bf8caead4..53762a0f7 100644 --- a/cmd/mapt/cmd/params/params.go +++ b/cmd/mapt/cmd/params/params.go @@ -1,6 +1,9 @@ package params import ( + "fmt" + "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" @@ -288,18 +291,32 @@ func AddGHActionsFlags(fs *pflag.FlagSet) { func GithubRunnerArgs() *github.GithubRunnerArgs { if viper.IsSet(ghActionsRunnerToken) { + 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: viper.GetString(ghActionsRunnerToken), RepoURL: viper.GetString(ghActionsRunnerRepo), Labels: viper.GetStringSlice(ghActionsRunnerLabels), Platform: &github.Linux, Arch: linuxArchAsGithubActionsArch(viper.GetString(LinuxArch)), - RunnerImageRepo: viper.GetString(ghActionsRunnerImageRepo), + RunnerImageRepo: imageRepo, } } return nil } +func validateRunnerImageRepo(repo string) error { + if !strings.HasPrefix(repo, "https://") { + return fmt.Errorf("only HTTPS URLs are allowed, got: %s", repo) + } + return nil +} + func AddCirrusFlags(fs *pflag.FlagSet) { fs.StringP(cirrusPWToken, "", "", cirrusPWTokenDesc) fs.StringToStringP(cirrusPWLabels, "", nil, cirrusPWLabelsDesc) diff --git a/pkg/integrations/github/snippet-linux-ppc64le.sh b/pkg/integrations/github/snippet-linux-ppc64le.sh index 5c26bf1a1..7d32e80d5 100644 --- a/pkg/integrations/github/snippet-linux-ppc64le.sh +++ b/pkg/integrations/github/snippet-linux-ppc64le.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -git clone {{ .RunnerImageRepo }} /opt/action-runner-image-pz +git clone --depth=1 "{{ .RunnerImageRepo }}" /opt/action-runner-image-pz cd /opt/action-runner-image-pz bash -c '. scripts/vm.sh rhel 9 minimal --skip-snap-lxd' diff --git a/pkg/integrations/github/snippet-linux-s390x.sh b/pkg/integrations/github/snippet-linux-s390x.sh index f11e43e1b..7dc116481 100644 --- a/pkg/integrations/github/snippet-linux-s390x.sh +++ b/pkg/integrations/github/snippet-linux-s390x.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -git clone {{ .RunnerImageRepo }} /opt/action-runner-image-pz +git clone --depth=1 "{{ .RunnerImageRepo }}" /opt/action-runner-image-pz cd /opt/action-runner-image-pz bash -c '. scripts/vm.sh ubuntu 22.04 minimal --skip-snap-lxd' From f68b50986a3b7661e7fc39c6f6e623adc8a9bc97 Mon Sep 17 00:00:00 2001 From: Dev Kumar Date: Tue, 9 Jun 2026 16:40:43 -0400 Subject: [PATCH 03/13] feat(ibmcloud): auto-generate GitHub Actions runner registration token --- cmd/mapt/cmd/params/params.go | 52 +++++++++++++++++++-------- pkg/integrations/github/api.go | 66 ++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 15 deletions(-) create mode 100644 pkg/integrations/github/api.go diff --git a/cmd/mapt/cmd/params/params.go b/cmd/mapt/cmd/params/params.go index 53762a0f7..b543762b0 100644 --- a/cmd/mapt/cmd/params/params.go +++ b/cmd/mapt/cmd/params/params.go @@ -2,6 +2,7 @@ package params import ( "fmt" + "os" "strings" "github.com/redhat-developer/mapt/pkg/integrations/cirrus" @@ -290,24 +291,45 @@ func AddGHActionsFlags(fs *pflag.FlagSet) { } func GithubRunnerArgs() *github.GithubRunnerArgs { - if viper.IsSet(ghActionsRunnerToken) { - imageRepo := viper.GetString(ghActionsRunnerImageRepo) - if imageRepo != "" { - if err := validateRunnerImageRepo(imageRepo); err != nil { - logging.Errorf("invalid --ghactions-runner-image-repo: %v", err) - return nil - } + 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 } - return &github.GithubRunnerArgs{ - Token: viper.GetString(ghActionsRunnerToken), - RepoURL: viper.GetString(ghActionsRunnerRepo), - Labels: viper.GetStringSlice(ghActionsRunnerLabels), - Platform: &github.Linux, - Arch: linuxArchAsGithubActionsArch(viper.GetString(LinuxArch)), - RunnerImageRepo: imageRepo, + 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 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 { 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 +} From 68f31f449f820b1dc3b0332bd8edfeb1ae2d7854 Mon Sep 17 00:00:00 2001 From: Dev Kumar Date: Wed, 10 Jun 2026 17:21:56 -0400 Subject: [PATCH 04/13] fix(ibmcloud): install prerequisites before runner image build --- pkg/integrations/github/snippet-linux-ppc64le.sh | 2 ++ pkg/integrations/github/snippet-linux-s390x.sh | 2 ++ 2 files changed, 4 insertions(+) diff --git a/pkg/integrations/github/snippet-linux-ppc64le.sh b/pkg/integrations/github/snippet-linux-ppc64le.sh index 7d32e80d5..26d513a73 100644 --- a/pkg/integrations/github/snippet-linux-ppc64le.sh +++ b/pkg/integrations/github/snippet-linux-ppc64le.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash set -euo pipefail +dnf install -y git-core + git clone --depth=1 "{{ .RunnerImageRepo }}" /opt/action-runner-image-pz cd /opt/action-runner-image-pz diff --git a/pkg/integrations/github/snippet-linux-s390x.sh b/pkg/integrations/github/snippet-linux-s390x.sh index 7dc116481..49323f30c 100644 --- a/pkg/integrations/github/snippet-linux-s390x.sh +++ b/pkg/integrations/github/snippet-linux-s390x.sh @@ -1,6 +1,8 @@ #!/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 From 78d423b7f65376bd4eb512746b13a7898fa4e962 Mon Sep 17 00:00:00 2001 From: Dev Kumar Date: Wed, 10 Jun 2026 18:07:41 -0400 Subject: [PATCH 05/13] ci: add s390x runner smoke test workflow Co-Authored-By: Claude Opus 4.6 --- .github/workflows/smoke-test-s390x.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/workflows/smoke-test-s390x.yaml 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)" From 753b7efe35936ee7bb147538e85a4abc9dc7cdfc Mon Sep 17 00:00:00 2001 From: Dev Kumar Date: Wed, 10 Jun 2026 18:16:32 -0400 Subject: [PATCH 06/13] fix(ibmcloud): tolerate flaky upstream test failures in runner build --- pkg/integrations/github/snippet-linux-ppc64le.sh | 8 +++++++- pkg/integrations/github/snippet-linux-s390x.sh | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pkg/integrations/github/snippet-linux-ppc64le.sh b/pkg/integrations/github/snippet-linux-ppc64le.sh index 26d513a73..16348fe3c 100644 --- a/pkg/integrations/github/snippet-linux-ppc64le.sh +++ b/pkg/integrations/github/snippet-linux-ppc64le.sh @@ -6,7 +6,13 @@ dnf install -y git-core git clone --depth=1 "{{ .RunnerImageRepo }}" /opt/action-runner-image-pz cd /opt/action-runner-image-pz -bash -c '. scripts/vm.sh rhel 9 minimal --skip-snap-lxd' +# Allow build to continue past flaky upstream test failures +bash -c '. scripts/vm.sh rhel 9 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 cd /opt/runner-cache export DOTNET_ROOT=/opt/dotnet diff --git a/pkg/integrations/github/snippet-linux-s390x.sh b/pkg/integrations/github/snippet-linux-s390x.sh index 49323f30c..60dadbc67 100644 --- a/pkg/integrations/github/snippet-linux-s390x.sh +++ b/pkg/integrations/github/snippet-linux-s390x.sh @@ -6,7 +6,13 @@ 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 -bash -c '. scripts/vm.sh ubuntu 22.04 minimal --skip-snap-lxd' +# 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 cd /opt/runner-cache export DOTNET_ROOT=/opt/dotnet From 9e03696e8ee6b0253aaa0b66989e7f6107eb51d1 Mon Sep 17 00:00:00 2001 From: Dev Kumar Date: Wed, 10 Jun 2026 22:15:53 -0400 Subject: [PATCH 07/13] fix(ibmcloud): preserve runner binary if post-build installer fails --- pkg/integrations/github/snippet-linux-ppc64le.sh | 12 +++++++++++- pkg/integrations/github/snippet-linux-s390x.sh | 12 +++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/pkg/integrations/github/snippet-linux-ppc64le.sh b/pkg/integrations/github/snippet-linux-ppc64le.sh index 16348fe3c..ac91cd369 100644 --- a/pkg/integrations/github/snippet-linux-ppc64le.sh +++ b/pkg/integrations/github/snippet-linux-ppc64le.sh @@ -6,8 +6,18 @@ dnf install -y git-core 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 +# Snapshot the runner binary as soon as it is built; a later installer +# failure (e.g. Docker GPG key) can trigger cleanup that deletes it. +(while [ ! -f /opt/runner-cache/config.sh ]; do sleep 10; done + cp -a /opt/runner-cache /opt/runner-backup) & +WATCHER_PID=$! + bash -c '. scripts/vm.sh rhel 9 minimal --skip-snap-lxd' || true +kill $WATCHER_PID 2>/dev/null || true + +if [ ! -f /opt/runner-cache/config.sh ] && [ -d /opt/runner-backup ]; then + mv /opt/runner-backup /opt/runner-cache +fi if [ ! -f /opt/runner-cache/config.sh ]; then echo "Runner binary not found after build — check build logs" >&2 diff --git a/pkg/integrations/github/snippet-linux-s390x.sh b/pkg/integrations/github/snippet-linux-s390x.sh index 60dadbc67..61da42b9e 100644 --- a/pkg/integrations/github/snippet-linux-s390x.sh +++ b/pkg/integrations/github/snippet-linux-s390x.sh @@ -6,8 +6,18 @@ 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 +# Snapshot the runner binary as soon as it is built; a later installer +# failure (e.g. Docker GPG key) can trigger cleanup that deletes it. +(while [ ! -f /opt/runner-cache/config.sh ]; do sleep 10; done + cp -a /opt/runner-cache /opt/runner-backup) & +WATCHER_PID=$! + bash -c '. scripts/vm.sh ubuntu 22.04 minimal --skip-snap-lxd' || true +kill $WATCHER_PID 2>/dev/null || true + +if [ ! -f /opt/runner-cache/config.sh ] && [ -d /opt/runner-backup ]; then + mv /opt/runner-backup /opt/runner-cache +fi if [ ! -f /opt/runner-cache/config.sh ]; then echo "Runner binary not found after build — check build logs" >&2 From 782906200033af50c0c9e1497d00685d9be4c927 Mon Sep 17 00:00:00 2001 From: Dev Kumar Date: Wed, 10 Jun 2026 22:30:13 -0400 Subject: [PATCH 08/13] revert: remove unnecessary runner binary watcher from snippets --- pkg/integrations/github/snippet-linux-ppc64le.sh | 12 +----------- pkg/integrations/github/snippet-linux-s390x.sh | 12 +----------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/pkg/integrations/github/snippet-linux-ppc64le.sh b/pkg/integrations/github/snippet-linux-ppc64le.sh index ac91cd369..16348fe3c 100644 --- a/pkg/integrations/github/snippet-linux-ppc64le.sh +++ b/pkg/integrations/github/snippet-linux-ppc64le.sh @@ -6,18 +6,8 @@ dnf install -y git-core git clone --depth=1 "{{ .RunnerImageRepo }}" /opt/action-runner-image-pz cd /opt/action-runner-image-pz -# Snapshot the runner binary as soon as it is built; a later installer -# failure (e.g. Docker GPG key) can trigger cleanup that deletes it. -(while [ ! -f /opt/runner-cache/config.sh ]; do sleep 10; done - cp -a /opt/runner-cache /opt/runner-backup) & -WATCHER_PID=$! - +# Allow build to continue past flaky upstream test failures bash -c '. scripts/vm.sh rhel 9 minimal --skip-snap-lxd' || true -kill $WATCHER_PID 2>/dev/null || true - -if [ ! -f /opt/runner-cache/config.sh ] && [ -d /opt/runner-backup ]; then - mv /opt/runner-backup /opt/runner-cache -fi if [ ! -f /opt/runner-cache/config.sh ]; then echo "Runner binary not found after build — check build logs" >&2 diff --git a/pkg/integrations/github/snippet-linux-s390x.sh b/pkg/integrations/github/snippet-linux-s390x.sh index 61da42b9e..60dadbc67 100644 --- a/pkg/integrations/github/snippet-linux-s390x.sh +++ b/pkg/integrations/github/snippet-linux-s390x.sh @@ -6,18 +6,8 @@ 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 -# Snapshot the runner binary as soon as it is built; a later installer -# failure (e.g. Docker GPG key) can trigger cleanup that deletes it. -(while [ ! -f /opt/runner-cache/config.sh ]; do sleep 10; done - cp -a /opt/runner-cache /opt/runner-backup) & -WATCHER_PID=$! - +# Allow build to continue past flaky upstream test failures bash -c '. scripts/vm.sh ubuntu 22.04 minimal --skip-snap-lxd' || true -kill $WATCHER_PID 2>/dev/null || true - -if [ ! -f /opt/runner-cache/config.sh ] && [ -d /opt/runner-backup ]; then - mv /opt/runner-backup /opt/runner-cache -fi if [ ! -f /opt/runner-cache/config.sh ]; then echo "Runner binary not found after build — check build logs" >&2 From a6893f04c899c6e1b878a3a1de20b044ce7a28a2 Mon Sep 17 00:00:00 2001 From: Dev Kumar Date: Wed, 10 Jun 2026 23:02:46 -0400 Subject: [PATCH 09/13] fix(ibmcloud): run GitHub Actions runner as non-root user --- .../github/snippet-linux-ppc64le.sh | 33 +++++++++++-------- .../github/snippet-linux-s390x.sh | 33 +++++++++++-------- 2 files changed, 38 insertions(+), 28 deletions(-) diff --git a/pkg/integrations/github/snippet-linux-ppc64le.sh b/pkg/integrations/github/snippet-linux-ppc64le.sh index 16348fe3c..d54eba29e 100644 --- a/pkg/integrations/github/snippet-linux-ppc64le.sh +++ b/pkg/integrations/github/snippet-linux-ppc64le.sh @@ -14,17 +14,22 @@ if [ ! -f /opt/runner-cache/config.sh ]; then exit 1 fi -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 > /var/log/gh-runner.log 2>&1 & +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/snippet-linux-s390x.sh b/pkg/integrations/github/snippet-linux-s390x.sh index 60dadbc67..22bd2982d 100644 --- a/pkg/integrations/github/snippet-linux-s390x.sh +++ b/pkg/integrations/github/snippet-linux-s390x.sh @@ -14,17 +14,22 @@ if [ ! -f /opt/runner-cache/config.sh ]; then exit 1 fi -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 > /var/log/gh-runner.log 2>&1 & +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 & +' From 60e93b8a80b11296d2b5bcc7687c719e581f3b19 Mon Sep 17 00:00:00 2001 From: Dev Kumar Date: Thu, 11 Jun 2026 14:38:55 -0400 Subject: [PATCH 10/13] fix(ibmcloud): repair PAM config after runner build to preserve sshd The upstream configure-limits.sh appends duplicate pam_limits.so entries to system-auth and password-auth, causing sshd to drop connections before sending its banner. Deduplicate PAM entries and restart sshd after build. Co-Authored-By: Claude Opus 4.6 --- pkg/integrations/github/snippet-linux-ppc64le.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/integrations/github/snippet-linux-ppc64le.sh b/pkg/integrations/github/snippet-linux-ppc64le.sh index d54eba29e..3f8bac531 100644 --- a/pkg/integrations/github/snippet-linux-ppc64le.sh +++ b/pkg/integrations/github/snippet-linux-ppc64le.sh @@ -9,6 +9,15 @@ 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 +# The upstream configure-limits.sh appends duplicate pam_limits.so entries +# which breaks sshd (connection drops before banner). Deduplicate them. +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 + if [ ! -f /opt/runner-cache/config.sh ]; then echo "Runner binary not found after build — check build logs" >&2 exit 1 From ef87753009029b76a2a5bcf3dc21b57b1b4ec0be Mon Sep 17 00:00:00 2001 From: Dev Kumar Date: Thu, 11 Jun 2026 15:46:42 -0400 Subject: [PATCH 11/13] debug(ibmcloud): add sshd watchdog and diagnostic logging to ppc64le snippet Adds a background monitor that logs sshd status every 30s during the runner build to identify what breaks SSH. After build completion, dumps full sshd diagnostics (config test, journal, host key perms, crypto policies, PAM config) and attempts repair. Co-Authored-By: Claude Opus 4.6 --- .../github/snippet-linux-ppc64le.sh | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/pkg/integrations/github/snippet-linux-ppc64le.sh b/pkg/integrations/github/snippet-linux-ppc64le.sh index 3f8bac531..d8ab808d2 100644 --- a/pkg/integrations/github/snippet-linux-ppc64le.sh +++ b/pkg/integrations/github/snippet-linux-ppc64le.sh @@ -3,21 +3,51 @@ 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 -# The upstream configure-limits.sh appends duplicate pam_limits.so entries -# which breaks sshd (connection drops before banner). Deduplicate them. +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 + +# Attempt sshd repair: fix PAM duplicates, restore permissions, restart 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 +chmod 600 /etc/ssh/ssh_host_*_key 2>/dev/null || true +chmod 644 /etc/ssh/ssh_host_*_key.pub 2>/dev/null || true +find /usr/share/crypto-policies/ -type f -exec chmod 644 {} + 2>/dev/null || true +find /usr/share/crypto-policies/ -type d -exec chmod 755 {} + 2>/dev/null || true 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 + if [ ! -f /opt/runner-cache/config.sh ]; then echo "Runner binary not found after build — check build logs" >&2 exit 1 From 265345e21be4c661f8e227404c380a616d9a4679 Mon Sep 17 00:00:00 2001 From: Dev Kumar Date: Thu, 11 Jun 2026 16:46:55 -0400 Subject: [PATCH 12/13] fix(ibmcloud): restore sshd privsep dir after runner build + COS diagnostics The upstream configure-system.sh runs chmod -R 777 /usr/share which makes /usr/share/empty.sshd (sshd's privilege separation directory) world-writable. sshd refuses to start when this directory is not owned by root or is world-writable. Fix by restoring 755 after the build. Also adds sshd watchdog logging with COS upload so diagnostics are accessible even when SSH is broken. COS credentials are passed through cloud-config template variables. Co-Authored-By: Claude Opus 4.6 --- .../github/snippet-linux-ppc64le.sh | 53 +++++++++++++++++-- .../ibmcloud/action/ibm-power/cloud-config | 6 ++- .../ibmcloud/action/ibm-power/ibm-power.go | 7 +++ 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/pkg/integrations/github/snippet-linux-ppc64le.sh b/pkg/integrations/github/snippet-linux-ppc64le.sh index d8ab808d2..72b5d2f63 100644 --- a/pkg/integrations/github/snippet-linux-ppc64le.sh +++ b/pkg/integrations/github/snippet-linux-ppc64le.sh @@ -32,22 +32,65 @@ 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 -# Attempt sshd repair: fix PAM duplicates, restore permissions, restart +# 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 -chmod 600 /etc/ssh/ssh_host_*_key 2>/dev/null || true -chmod 644 /etc/ssh/ssh_host_*_key.pub 2>/dev/null || true -find /usr/share/crypto-policies/ -type f -exec chmod 644 {} + 2>/dev/null || true -find /usr/share/crypto-policies/ -type d -exec chmod 755 {} + 2>/dev/null || true 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 diff --git a/pkg/provider/ibmcloud/action/ibm-power/cloud-config b/pkg/provider/ibmcloud/action/ibm-power/cloud-config index 18efe36cd..83982e7bd 100644 --- a/pkg/provider/ibmcloud/action/ibm-power/cloud-config +++ b/pkg/provider/ibmcloud/action/ibm-power/cloud-config @@ -106,5 +106,9 @@ runcmd: - bash /opt/install-glrunner.sh {{- end}} {{- if .GHActionsRunnerScript}} - - bash /opt/install-ghrunner.sh + - | + 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 a4618ae88..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" @@ -34,6 +35,9 @@ type userDataValues struct { OtelColScript string GitLabRunnerScript string GHActionsRunnerScript string + COSAccessKeyID string + COSSecretAccessKey string + COSEndpoint string } const ( @@ -482,6 +486,9 @@ func piUserData(gateway string, otelArgs *otelcol.OtelcolArgs, glRunnerScript, g 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 { From de8fc1f13186bce5b044814461fe3f103f175e42 Mon Sep 17 00:00:00 2001 From: Dev Kumar Date: Thu, 11 Jun 2026 17:51:29 -0400 Subject: [PATCH 13/13] fix(ibmcloud): remove nonexistent /opt/dotnet from ppc64le snippet On RHEL 9/ppc64le, dotnet installs to /usr/lib64/dotnet via dnf, not /opt/dotnet. The GH runner is self-contained (uses ./bin/Runner.Listener) and does not need DOTNET_ROOT. The chown on /opt/dotnet caused cloud-init to fail after a successful build. Co-Authored-By: Claude Opus 4.6 --- pkg/integrations/github/snippet-linux-ppc64le.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/integrations/github/snippet-linux-ppc64le.sh b/pkg/integrations/github/snippet-linux-ppc64le.sh index 72b5d2f63..bd5da65ad 100644 --- a/pkg/integrations/github/snippet-linux-ppc64le.sh +++ b/pkg/integrations/github/snippet-linux-ppc64le.sh @@ -97,12 +97,10 @@ if [ ! -f /opt/runner-cache/config.sh ]; then fi id -u runner &>/dev/null || useradd -m -s /bin/bash runner -chown -R runner:runner /opt/runner-cache /opt/dotnet +chown -R runner:runner /opt/runner-cache sudo -u runner bash -c ' cd /opt/runner-cache - export DOTNET_ROOT=/opt/dotnet - export PATH=$PATH:$DOTNET_ROOT ./config.sh \ --unattended \