Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
174005a
fix: add log level validation and support 0-10 range
AnnaZivkovic Apr 10, 2026
3a795a9
fix: change early return to continue in resource processing loop
AnnaZivkovic Apr 10, 2026
c719ecd
fix: return unmodified pod on PPC list error (true fail-open)
AnnaZivkovic Apr 10, 2026
be82e5d
fix: make PodPlacementConfig.spec.plugins optional for backward compa…
AnnaZivkovic Apr 10, 2026
a726c29
fix: enforce singleton name 'cluster' in ClusterPodPlacementConfig we…
AnnaZivkovic Apr 10, 2026
9697630
fix: make cache-empty assertion eventual to prevent test flakes
AnnaZivkovic Apr 13, 2026
3c63578
fix: close HTTP response body in suite test
AnnaZivkovic Apr 13, 2026
1013371
fix: use DeferCleanup to restore CPPC in fallback architecture test
AnnaZivkovic Apr 13, 2026
1a1ce4d
fix: add separate metric for stale ENoExecEvents (NotFound cases)
AnnaZivkovic Apr 13, 2026
32aa415
fix: propagate failed ENoExecEvent delete errors during cleanup
AnnaZivkovic Apr 13, 2026
053ea2b
docs: update vendoring instructions to use make vendor
AnnaZivkovic Apr 13, 2026
2e121f9
fix: preserve Kubernetes patch version in fallback discovery
AnnaZivkovic Apr 13, 2026
ac697ef
fix: exclude prerelease tags from version discovery
AnnaZivkovic Apr 13, 2026
532f030
fix: error on go version mismatch instead of downgrading
AnnaZivkovic Apr 13, 2026
c51015b
fix: improve upgrade automation script robustness and portability
AnnaZivkovic Apr 13, 2026
3ebcdac
docs: correct namespace exclusion behavior in CLAUDE.md
AnnaZivkovic Apr 13, 2026
d0a59ca
fix: correct typo in EnoexecCounterInvalid metric help text
AnnaZivkovic Apr 13, 2026
788506d
fix: correct typo in copyright header
AnnaZivkovic Apr 13, 2026
021d151
docs: add language tags to fenced code blocks in upgrade README
AnnaZivkovic Apr 13, 2026
7d7f96e
docs: document fail-open behavior in pullSecretDataList
AnnaZivkovic Apr 14, 2026
5f35741
fix: use Eventually for cache assertion after CPPC deletion
AnnaZivkovic Apr 14, 2026
543aa78
test: mark placeholder tests as pending to prevent false passes
AnnaZivkovic Apr 14, 2026
16fbc42
test: add GinkgoRecover to manager startup goroutines
AnnaZivkovic Apr 14, 2026
1a0c75d
fix: separate mktemp declaration to catch failures
AnnaZivkovic Apr 14, 2026
4de0be3
fix: add gosec nolint for validated int8 conversion
AnnaZivkovic Apr 14, 2026
ba7b0cb
fix: add nil check for cppc in shouldIgnorePod
AnnaZivkovic Apr 14, 2026
24fe75f
fix: suppress linter warnings for unexported fields with json tags
AnnaZivkovic Apr 14, 2026
8a413d5
fix: replace deprecated BearerTokenFile with Authorization in Service…
AnnaZivkovic Apr 14, 2026
3fc6bd5
fix: replace deprecated NewSimpleClientset with NewClientset
AnnaZivkovic Apr 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,8 @@ The operator binary runs in four mutually exclusive modes (controlled by flags i
**Mutating Webhook** (`internal/controller/podplacement/scheduling_gate_mutating_webhook.go`):
- Adds `multiarch.openshift.io/scheduling-gate` to new pods
- Respects namespace selector from ClusterPodPlacementConfig
- Always excludes: `openshift-*`, `kube-*`, `hypershift-*` namespaces
- Always excludes: `kube-*` namespaces (hardcoded)
- Other namespaces (`openshift-*`, `hypershift-*`, etc.) can be included via namespaceSelector configuration
- Uses worker pool for event publishing (ants library, 16 workers)

**Image Inspector** (`pkg/image/inspector.go`):
Expand Down Expand Up @@ -244,12 +245,15 @@ The operator binary runs in four mutually exclusive modes (controlled by flags i

### Namespace Exclusions

System namespaces are excluded from pod placement by default (cannot be overridden):
- `openshift-*`
- `kube-*`
- `hypershift-*`
**Hardcoded exclusions** (cannot be overridden):
- `kube-*` - Core Kubernetes namespaces are always excluded

Additional namespaces can be excluded via namespaceSelector with label `multiarch.openshift.io/exclude-pod-placement`.
**Configurable via namespaceSelector:**
- All other namespaces (including `openshift-*`, `hypershift-*`, user namespaces) can be included or excluded by configuring the `namespaceSelector` field in ClusterPodPlacementConfig
- Common exclusion pattern: add label `multiarch.openshift.io/exclude-pod-placement` to namespaces you want to skip
- Default example namespaceSelector excludes namespaces with this label

The operator namespace is also always excluded from pod placement.

### Plugins System

Expand Down Expand Up @@ -340,7 +344,7 @@ RUNTIME_IMAGE=<custom-image> # Override runtime base image
## Important Constraints

- ClusterPodPlacementConfig must be named "cluster" (singleton enforced by webhook)
- Namespaces `openshift-*`, `kube-*`, and `hypershift-*` are always excluded from pod placement
- Only `kube-*` namespaces are hardcoded as always excluded from pod placement; other system namespaces (`openshift-*`, `hypershift-*`) can be included via namespaceSelector configuration
- CGO is required for building (uses gpgme for registry authentication)
- Only one execution mode flag can be set at a time in main.go
- The operator uses vendored dependencies (`GOFLAGS=-mod=vendor`)
Expand Down
1 change: 1 addition & 0 deletions api/v1beta1/clusterpodplacementconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type ClusterPodPlacementConfigStatus struct {
Conditions []metav1.Condition `json:"conditions,omitempty"`

// The following fields are used to derive the conditions. They are not exposed to the user.
// nolint:staticcheck,revive // controller-gen requires json tags on all fields, even unexported ones
available bool `json:"-"`
progressing bool `json:"-"`
degraded bool `json:"-"`
Expand Down
5 changes: 5 additions & 0 deletions api/v1beta1/clusterpodplacementconfig_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (
"errors"
"fmt"

"github.com/openshift/multiarch-tuning-operator/api/common"

"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand Down Expand Up @@ -72,6 +74,9 @@ func (v *ClusterPodPlacementConfigValidator) validate(obj runtime.Object) (warni
if !ok {
return nil, errors.New("not a ClusterPodPlacementConfig")
}
if cppc.Name != common.SingletonResourceObjectName {
return nil, fmt.Errorf("ClusterPodPlacementConfig must be named %q", common.SingletonResourceObjectName)
}
if cppc.Spec.Plugins == nil || cppc.Spec.Plugins.NodeAffinityScoring == nil {
return nil, nil
}
Expand Down
5 changes: 2 additions & 3 deletions api/v1beta1/podplacementconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,8 @@ type PodPlacementConfigSpec struct {
LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"`

// Plugins defines the configurable plugins for this component.
// This field is required.
// +kubebuilder:validation:Required
Plugins *plugins.LocalPlugins `json:"plugins"`
// +optional
Plugins *plugins.LocalPlugins `json:"plugins,omitempty"`

// Priority defines the priority of the PodPlacementConfig and only accepts values in the range 0-255.
// This field is optional and will default to 0 if not set.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ metadata:
categories: OpenShift Optional, Other
console.openshift.io/disable-operand-delete: "false"
containerImage: registry.ci.openshift.org/origin/multiarch-tuning-operator:main
createdAt: "2026-03-02T21:50:07Z"
createdAt: "2026-04-14T00:14:34Z"
features.operators.openshift.io/cnf: "false"
features.operators.openshift.io/cni: "false"
features.operators.openshift.io/csi: "false"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,7 @@ spec:
type: object
x-kubernetes-map-type: atomic
plugins:
description: |-
Plugins defines the configurable plugins for this component.
This field is required.
description: Plugins defines the configurable plugins for this component.
properties:
nodeAffinityScoring:
description: NodeAffinityScoring is the plugin that implements
Expand Down Expand Up @@ -144,8 +142,6 @@ spec:
maximum: 255
minimum: 0
type: integer
required:
- plugins
type: object
status:
description: PodPlacementConfigStatus defines the observed state of PodPlacementConfig
Expand Down
7 changes: 5 additions & 2 deletions cmd/enoexec-daemon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,19 @@ func main() {
}

func bindFlags() {
flag.IntVar(&initialLogLevel, "initial-log-level", 0, "Initial log level. From 0 (Normal) to 5 (TraceAll)")
flag.IntVar(&initialLogLevel, "initial-log-level", 0, "Initial log level. From 0 (Normal) to 10 (maximum verbosity)")
flag.BoolVar(&logDevMode, "log-dev-mode", false, "Enable development mode for zap logger")
flag.Parse()
}

func initContext() (context.Context, context.CancelFunc) {
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
if initialLogLevel < 0 || initialLogLevel > 10 {
must(fmt.Errorf("initial-log-level must be in range [0,10], got %d", initialLogLevel), "invalid flag value")
}
var logImpl *zap.Logger
var err error
logLevel := zapcore.Level(int8(-initialLogLevel)) // #nosec G115 -- initialLogLevel is constrained to 0-5 range
logLevel := zapcore.Level(-initialLogLevel)
if logDevMode {
cfg := zap.NewDevelopmentConfig()
cfg.Level = zap.NewAtomicLevelAt(logLevel)
Expand Down
9 changes: 7 additions & 2 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,9 @@ func validateFlags() error {
if btoi(enableOperator)+btoi(enableClusterPodPlacementConfigOperandControllers)+btoi(enableClusterPodPlacementConfigOperandWebHook)+btoi(enableENoExecEventControllers) > 1 {
return errors.New("only one of the following flags can be set: --enable-operator, --enable-ppc-controllers, --enable-ppc-webhook, --enable-enoexec-event-controllers")
}
if initialLogLevel < 0 || initialLogLevel > 10 {
return fmt.Errorf("initial-log-level must be in range [0,10], got %d", initialLogLevel)
}
return nil
}

Expand All @@ -319,11 +322,13 @@ func bindFlags() {
// This may be deprecated in the future. It is used to support the current way of setting the log level for operands
// If operands will start to support a controller that watches the ClusterPodPlacementConfig, this flag may be removed
// and the log level will be set in the ClusterPodPlacementConfig at runtime (with no need for reconciliation)
flag.IntVar(&initialLogLevel, "initial-log-level", common.LogVerbosityLevelNormal.ToZapLevelInt(), "Initial log level. Converted to zap")
flag.IntVar(&initialLogLevel, "initial-log-level", common.LogVerbosityLevelNormal.ToZapLevelInt(), "Initial log level (0-10). Converted to zap. CRD levels: 0=Normal, 1=Debug, 2=Trace, 3=TraceAll")
klog.InitFlags(nil)
flag.Parse()
// Set the Log Level as AtomicLevel to allow runtime changes
utils.AtomicLevel = zapuber.NewAtomicLevelAt(zapcore.Level(int8(-initialLogLevel))) // #nosec G115 -- initialLogLevel is constrained to 0-3 range
// Safe to convert: initialLogLevel is validated to be in range [0,10] by validateFlags(),
// so -initialLogLevel is in range [-10,0] which fits in int8 range [-128,127]
utils.AtomicLevel = zapuber.NewAtomicLevelAt(zapcore.Level(int8(-initialLogLevel))) //nolint:gosec // G115 - conversion is safe, value validated to be in range
zapLogger := zap.New(zap.Level(utils.AtomicLevel), zap.UseDevMode(false))
klog.SetLogger(zapLogger)
ctrllog.SetLogger(zapLogger)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,7 @@ spec:
type: object
x-kubernetes-map-type: atomic
plugins:
description: |-
Plugins defines the configurable plugins for this component.
This field is required.
description: Plugins defines the configurable plugins for this component.
properties:
nodeAffinityScoring:
description: NodeAffinityScoring is the plugin that implements
Expand Down Expand Up @@ -144,8 +142,6 @@ spec:
maximum: 255
minimum: 0
type: integer
required:
- plugins
type: object
status:
description: PodPlacementConfigStatus defines the observed state of PodPlacementConfig
Expand Down
15 changes: 7 additions & 8 deletions hack/upgrade-automation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,10 @@ Updates all Go dependencies (smart handling skips incompatible versions):

Re-vendors all dependencies:
1. `go mod tidy`
2. `rm -rf vendor/`
3. `go mod vendor`
4. Restores go directive if `go mod tidy` upgraded it
2. `make vendor`
3. Restores go directive if `go mod tidy` upgraded it

**Commit:** `go mod vendor`
**Commit:** `make vendor`

### Step 5: Run code generation

Expand Down Expand Up @@ -220,7 +219,7 @@ sed_inplace() {
| `validate_prerequisites()` | All above checks | Exit on any failure |

**Branch naming:**
```
```text
upgrade-ocp-{ocp}-go-{go_minor}-k8s-{k8s_minor}

Example: upgrade-ocp-4.20-go-1.24-k8s-1.34
Expand Down Expand Up @@ -277,19 +276,19 @@ hack/upgrade-automation/scripts/upgrade.sh 4.20 1.24 1.34.1
**Common causes and fixes:**

**API deprecation:**
```
```text
Error: undefined: corev1.SomeOldAPI
```
Solution: Update code to use new API (check K8s release notes)

**Test helper changes:**
```
```text
Error: cannot use X (type Y) as type Z
```
Solution: Update test setup code for new types

**Import path changes:**
```
```text
Error: package X is not in GOROOT
```
Solution: Update import paths (check go.mod for correct versions)
Expand Down
22 changes: 17 additions & 5 deletions hack/upgrade-automation/scripts/lib/validations.sh
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ validate_and_create_branch() {
read -p "Do you want to delete and recreate it? (y/N) " -n 1 -r >&2
echo "" >&2
if [[ $REPLY =~ ^[Yy]$ ]]; then
git branch -D "$branch_name"
git checkout -b "$branch_name"
# Use checkout -B to force recreation (works even if currently checked out)
git checkout -B "$branch_name"
else
echo "ERROR: Branch '$branch_name' already exists. Please delete it or choose a different version." >&2
return 1
Expand Down Expand Up @@ -124,12 +124,12 @@ validate_k8s_version_consistency() {
echo "Validating k8s.io version consistency in go.mod..." >&2

local versions_count
versions_count=$(grep '^\s*k8s\.io/' go.mod | grep -v '//' | awk '{print $2}' | grep -oP 'v\d+\.\d+' | sort -u | wc -l)
versions_count=$(grep '^\s*k8s\.io/' go.mod | grep -v '//' | awk '{print $2}' | sed -E 's/(v[0-9]+\.[0-9]+).*/\1/' | sort -u | wc -l)

if [[ "$versions_count" -gt 1 ]]; then
echo "⚠️ WARNING: Multiple k8s.io minor versions detected in go.mod" >&2
echo " Found versions:" >&2
grep '^\s*k8s\.io/' go.mod | grep -v '//' | awk '{print $1, $2}' | grep -oP 'v\d+\.\d+' | sort -u >&2
grep '^\s*k8s\.io/' go.mod | grep -v '//' | awk '{print $1, $2}' | sed -E 's/.*(v[0-9]+\.[0-9]+).*/\1/' | sort -u >&2
echo " This is expected for packages like k8s.io/klog and k8s.io/utils" >&2
else
echo "βœ… All k8s.io dependencies at consistent minor version" >&2
Expand Down Expand Up @@ -160,7 +160,7 @@ validate_prerequisites() {
local current_go current_k8s current_ocp
current_go=$(grep '^go ' go.mod | awk '{print $2}')
current_k8s=$(grep 'k8s.io/api' go.mod | head -1 | awk '{print $2}')
current_ocp=$(grep 'BUILD_IMAGE' Makefile | grep -oP 'openshift-\K[0-9]+\.[0-9]+')
current_ocp=$(grep 'BUILD_IMAGE' Makefile | sed -E 's/.*openshift-([0-9]+\.[0-9]+).*/\1/')

echo "Current versions:" >&2
echo " Go: $current_go" >&2
Expand All @@ -172,5 +172,17 @@ validate_prerequisites() {
echo " Kubernetes: $k8s_version" >&2
echo "" >&2

# Validate golang image exists
if ! validate_golang_image_exists "$go_version"; then
return 1
fi

echo "" >&2

# Validate k8s version consistency (warning only)
validate_k8s_version_consistency

echo "" >&2

return 0
}
27 changes: 14 additions & 13 deletions hack/upgrade-automation/scripts/lib/version-discovery.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ discover_k8s_from_ocp_release() {
echo "$k8s_version"
else
# Fallback: discover from openshift/api go.mod
local k8s_minor
k8s_minor=$(curl -sf "https://raw.githubusercontent.com/openshift/api/release-$ocp_version/go.mod" | \
grep 'k8s.io/api ' | awk '{print $2}' | grep -oP 'v0\.\K[0-9]+')
local k8s_module_version
k8s_module_version=$(curl -sf "https://raw.githubusercontent.com/openshift/api/release-$ocp_version/go.mod" | \
grep 'k8s.io/api ' | awk '{print $2}' | sed -E -n '/^v0\.[0-9]+\.[0-9]+$/p')

if [[ -n "$k8s_minor" ]]; then
echo "1.$k8s_minor.0"
if [[ -n "$k8s_module_version" ]]; then
# Transform v0.X.Y to 1.X.Y (preserving patch version)
echo "${k8s_module_version/v0./1.}"
else
echo ""
fi
Expand Down Expand Up @@ -74,7 +75,7 @@ discover_required_ocp_version() {
# Check each branch to find which uses our target K8s version
for ocp_version in $branches; do
local openshift_api_k8s
openshift_api_k8s=$(curl -sf "https://raw.githubusercontent.com/openshift/api/release-$ocp_version/go.mod" | grep 'k8s.io/api ' | awk '{print $2}' | grep -oP 'v0\.\K[0-9]+')
openshift_api_k8s=$(curl -sf "https://raw.githubusercontent.com/openshift/api/release-$ocp_version/go.mod" | grep 'k8s.io/api ' | awk '{print $2}' | sed -E 's/^v0\.([0-9]+).*/\1/')

if [[ "$openshift_api_k8s" == "$k8s_minor" ]]; then
echo "βœ… Found OCP $ocp_version uses K8s 1.$k8s_minor" >&2
Expand Down Expand Up @@ -113,7 +114,7 @@ validate_k8s_ocp_compatibility() {

# Check openshift/api release branch for K8s version
local openshift_api_k8s
openshift_api_k8s=$(curl -sf "https://raw.githubusercontent.com/openshift/api/release-$ocp_version/go.mod" | grep 'k8s.io/api ' | awk '{print $2}' | grep -oP 'v0\.\K[0-9]+')
openshift_api_k8s=$(curl -sf "https://raw.githubusercontent.com/openshift/api/release-$ocp_version/go.mod" | grep 'k8s.io/api ' | awk '{print $2}' | sed -E 's/^v0\.([0-9]+).*/\1/')

if [[ "$openshift_api_k8s" != "$k8s_minor" ]]; then
echo "ERROR: K8s version mismatch" >&2
Expand All @@ -139,7 +140,7 @@ discover_controller_runtime_version() {
echo "Discovering compatible controller-runtime version..." >&2

local releases
releases=$(curl -s https://api.github.com/repos/kubernetes-sigs/controller-runtime/releases | grep '"tag_name"' | grep -E '"v0\.' | sed -E 's/.*"v([^"]+)".*/\1/')
releases=$(curl -s https://api.github.com/repos/kubernetes-sigs/controller-runtime/releases | grep '"tag_name"' | grep -E '"v0\.' | grep -Ev -- '-(alpha|beta|rc)[0-9.]*"' | sed -E 's/.*"v([^"]+)".*/\1/')

for version in $releases; do
local gomod
Expand All @@ -150,7 +151,7 @@ discover_controller_runtime_version() {
fi

local cr_k8s_minor cr_go_minor
cr_k8s_minor=$(echo "$gomod" | grep 'k8s.io/apimachinery' | awk '{print $2}' | grep -oP 'v0\.\K[0-9]+' | head -1)
cr_k8s_minor=$(echo "$gomod" | grep 'k8s.io/apimachinery' | awk '{print $2}' | sed -E 's/^v0\.([0-9]+).*/\1/' | head -1)
cr_go_minor=$(echo "$gomod" | grep '^go ' | awk '{print $2}' | cut -d. -f2)

if [[ "$cr_k8s_minor" == "$k8s_minor" ]] && [[ "$cr_go_minor" -le "$go_minor" ]]; then
Expand Down Expand Up @@ -200,13 +201,13 @@ discover_controller_tools_version() {

local k8s_minor go_minor
# Extract minor version from k8s_version parameter (e.g., "1.34.1" -> "34")
k8s_minor=$(echo "$k8s_version" | grep -oP '1\.\K[0-9]+')
k8s_minor=$(echo "$k8s_version" | sed -E 's/^1\.([0-9]+).*/\1/')
go_minor=$(echo "$go_version" | cut -d. -f2)

echo "Discovering compatible controller-tools version..." >&2

local releases
releases=$(curl -s https://api.github.com/repos/kubernetes-sigs/controller-tools/releases | grep '"tag_name"' | grep -E '"v0\.' | sed -E 's/.*"v([^"]+)".*/\1/')
releases=$(curl -s https://api.github.com/repos/kubernetes-sigs/controller-tools/releases | grep '"tag_name"' | grep -E '"v0\.' | grep -Ev -- '-(alpha|beta|rc)[0-9.]*"' | sed -E 's/.*"v([^"]+)".*/\1/')

for version in $releases; do
local gomod
Expand All @@ -217,7 +218,7 @@ discover_controller_tools_version() {
fi

local ct_k8s_minor ct_go_minor
ct_k8s_minor=$(echo "$gomod" | grep 'k8s.io/apimachinery' | awk '{print $2}' | grep -oP 'v0\.\K[0-9]+' | head -1)
ct_k8s_minor=$(echo "$gomod" | grep 'k8s.io/apimachinery' | awk '{print $2}' | sed -E 's/^v0\.([0-9]+).*/\1/' | head -1)
ct_go_minor=$(echo "$gomod" | grep '^go ' | awk '{print $2}' | cut -d. -f2)

if [[ "$ct_k8s_minor" == "$k8s_minor" ]] && [[ "$ct_go_minor" -le "$go_minor" ]]; then
Expand Down Expand Up @@ -292,7 +293,7 @@ discover_golangci_lint_version() {
echo "Discovering compatible golangci-lint version..." >&2

local releases
releases=$(curl -s https://api.github.com/repos/golangci/golangci-lint/releases | grep '"tag_name"' | sed -E 's/.*"v([^"]+)".*/\1/')
releases=$(curl -s https://api.github.com/repos/golangci/golangci-lint/releases | grep '"tag_name"' | grep -Ev -- '-(alpha|beta|rc)[0-9.]*"' | sed -E 's/.*"v([^"]+)".*/\1/')

for version in $releases; do
local go_req
Expand Down
Loading