Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 22 additions & 13 deletions hack/verify-min-k8s-version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.

# Verifies that the hardcoded Kubernetes minimum version in pkg/kind/kind.go
# and pkg/minikube/minikube.go matches DefaultKubernetesMinVersion in the
# version of knative.dev/pkg pinned in our go.sum. We keep a copy locally
# rather than importing the package to avoid pulling in client-go and its
# transitive dependency tree.
# Verifies that the hardcoded Kubernetes minimum version in pkg/kind/kind.go,
# pkg/minikube/minikube.go, and pkg/install/install.go (the recommended kubectl
# version) matches DefaultKubernetesMinVersion in the version of knative.dev/pkg
# pinned in our go.sum. We keep a copy locally rather than importing the package
# to avoid pulling in client-go and its transitive dependency tree.
#
# If --write is passed, the script rewrites the local files in place when
# they drift from upstream (used by the auto-bump workflow). Default is
Expand Down Expand Up @@ -61,27 +61,36 @@ kind_version="$(extract pkg/kind/kind.go \
's/.*kubernetesVersion[[:space:]]*=[[:space:]]*"kindest\/node:v([0-9.]+)".*/\1/p')"
minikube_version="$(extract pkg/minikube/minikube.go \
's/.*kubernetesVersion[[:space:]]*=[[:space:]]*"([0-9.]+)".*/\1/p')"
kubectl_version="$(extract pkg/install/install.go \
's/.*kubectlMinVersion[[:space:]]*=[[:space:]]*"([0-9.]+)".*/\1/p')"

# Each entry is name:var:version:file, where var is the Go variable name holding
# the version literal. The kind/minikube clusters and the kubectl-version gate in
# pkg/install all track DefaultKubernetesMinVersion; keeping them in one loop means
# a single upstream bump updates every site.
fail=0
for entry in "kind:$kind_version:pkg/kind/kind.go" "minikube:$minikube_version:pkg/minikube/minikube.go"; do
name="${entry%%:*}"
rest="${entry#*:}"
for entry in \
"kind:kubernetesVersion:$kind_version:pkg/kind/kind.go" \
"minikube:kubernetesVersion:$minikube_version:pkg/minikube/minikube.go" \
"kubectl:kubectlMinVersion:$kubectl_version:pkg/install/install.go"; do
name="${entry%%:*}"; rest="${entry#*:}"
var="${rest%%:*}"; rest="${rest#*:}"
local_version="${rest%%:*}"
file="${rest#*:}"
if [[ -z "$local_version" ]]; then
echo "ERROR: could not parse kubernetesVersion from $file" >&2
echo "ERROR: could not parse $var from $file" >&2
fail=1
elif [[ "$local_version" != "$upstream_version" ]]; then
if [[ $WRITE -eq 1 ]]; then
sed -i.bak -E "s/(kubernetesVersion[[:space:]]*=[[:space:]]*\"(kindest\/node:v)?)${local_version}/\1${upstream_version}/" "$REPO_ROOT/$file"
sed -i.bak -E "s/(${var}[[:space:]]*=[[:space:]]*\"(kindest\/node:v)?)${local_version}/\1${upstream_version}/" "$REPO_ROOT/$file"
rm -f "$REPO_ROOT/$file.bak"
echo "UPDATED: $name kubernetesVersion in $file: $local_version -> $upstream_version"
echo "UPDATED: $name $var in $file: $local_version -> $upstream_version"
else
echo "ERROR: $name kubernetesVersion in $file is $local_version, upstream is $upstream_version (knative/pkg@$ref)" >&2
echo "ERROR: $name $var in $file is $local_version, upstream is $upstream_version (knative/pkg@$ref)" >&2
fail=1
fi
else
echo "OK: $name kubernetesVersion ($local_version) matches upstream (knative/pkg@$ref)"
echo "OK: $name $var ($local_version) matches upstream (knative/pkg@$ref)"
fi
done

Expand Down
112 changes: 111 additions & 1 deletion pkg/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,47 @@ package install

import (
"fmt"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"time"
)

// kubectlMinVersion is the minimum kubectl version we recommend. We gate on it
// because waitForPodsReady relies on `kubectl wait --for=create` with a label
// selector, which only works correctly on kubectl >= 1.33 (see
// kubernetes/kubernetes#128662).
//
// kubectlMinVersion must match DefaultKubernetesMinVersion in
// https://github.com/knative/pkg/blob/main/version/version.go (without the
// leading "v"), matching the kind and minikube cluster versions. The
// verify-min-k8s-version CI check enforces this. We only compare the major and
// minor components at runtime; the patch component is kept so the literal
// matches upstream's format for that check.
var kubectlMinVersion = "1.34.0"

// kubectlGitVersion matches the "vMAJOR.MINOR" prefix of kubectl's reported
// git version, e.g. "v1.34.0" or "v1.33.2-eks-1234".
var kubectlGitVersion = regexp.MustCompile(`^v?(\d+)\.(\d+)`)

// kubectlMinMajor / kubectlMinMinor are parsed from kubectlMinVersion.
var kubectlMinMajor, kubectlMinMinor = mustParseMinVersion(kubectlMinVersion)

// mustParseMinVersion parses the leading MAJOR.MINOR out of kubectlMinVersion.
// It panics on a malformed literal, which can only happen if kubectlMinVersion
// is edited to an invalid value (a programming/CI error, caught by tests).
func mustParseMinVersion(v string) (int, int) {
m := kubectlGitVersion.FindStringSubmatch(v)
if m == nil {
panic(fmt.Sprintf("kn-plugin-quickstart: malformed kubectlMinVersion %q", v))
}
major, _ := strconv.Atoi(m[1])
minor, _ := strconv.Atoi(m[2])
return major, minor
}

// Component versions are generated at buildtime via the hack/build.sh script
var ServingVersion string
var KourierVersion string
Expand Down Expand Up @@ -238,9 +274,83 @@ func waitForCRDsEstablished() error {
return runCommand(exec.Command("kubectl", "wait", "--for=condition=Established", "--all", "crd"))
}

// CheckKubectlVersion validates that the user has a recent enough version of
// kubectl installed. If not, it warns the user and prompts them to continue,
// mirroring the behavior of the kind and minikube version checks.
func CheckKubectlVersion() error {
versionCheck := exec.Command("kubectl", "version", "--client", "-o", "json")
out, err := versionCheck.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to get kubectl version: %w", err)
}

major, minor, err := parseKubectlVersion(string(out))
if err != nil {
return fmt.Errorf("unable to parse kubectl version: %w", err)
}
fmt.Printf(" kubectl version is: v%d.%d\n", major, minor)

if major < kubectlMinMajor || (major == kubectlMinMajor && minor < kubectlMinMinor) {
var resp string
fmt.Printf("WARNING: We recommend at least kubectl v%d.%d, while you are using v%d.%d\n", kubectlMinMajor, kubectlMinMinor, major, minor)
fmt.Println("You can download a newer version from https://kubernetes.io/docs/tasks/tools/install-kubectl")
fmt.Print("Continue anyway? (not recommended) [y/N]: ")
fmt.Scanf("%s", &resp)
if resp != "y" && resp != "Y" {
fmt.Println("Installation stopped. Please upgrade kubectl and run again")
os.Exit(0)
}
}

return nil
}

// parseKubectlVersion extracts the client major and minor version from the
// JSON output of `kubectl version --client -o json`. It reads the gitVersion
// field (e.g. "v1.34.0") rather than the major/minor fields, which some
// distributions emit with non-numeric suffixes (e.g. minor "33+").
func parseKubectlVersion(jsonOut string) (int, int, error) {
// Pull gitVersion out of the JSON without a full struct decode so we stay
// resilient to extra fields; fall back to matching any vX.Y in the blob.
gitVersion := ""
if idx := strings.Index(jsonOut, `"gitVersion"`); idx >= 0 {
rest := jsonOut[idx:]
if start := strings.Index(rest, `:`); start >= 0 {
rest = rest[start+1:]
if open := strings.Index(rest, `"`); open >= 0 {
rest = rest[open+1:]
if close := strings.Index(rest, `"`); close >= 0 {
gitVersion = rest[:close]
}
}
}
}

m := kubectlGitVersion.FindStringSubmatch(gitVersion)
if m == nil {
return 0, 0, fmt.Errorf("could not find a version in kubectl output: %q", gitVersion)
}
major, err := strconv.Atoi(m[1])
if err != nil {
return 0, 0, err
}
minor, err := strconv.Atoi(m[2])
if err != nil {
return 0, 0, err
}
return major, minor, nil
}

// waitForPodsReady waits for all pods in the given namespace to be ready.
//
// We pass both --for=create and --for=condition=Ready because kubectl wait
// exits immediately with "no matching resources found" when no pods match the
// selector yet (e.g. the deployment controller hasn't created them). --for=create
// is always evaluated first, so this waits for the pods to appear and then for
// them to become Ready. This requires kubectl >= 1.33 for label-selector waits,
// which is within the versions this plugin already supports.
func waitForPodsReady(ns string) error {
return runCommand(exec.Command("kubectl", "wait", "pod", "--timeout=10m", "--for=condition=Ready", "-l", "!job-name", "-n", ns))
return runCommand(exec.Command("kubectl", "wait", "pod", "--timeout=10m", "--for=create", "--for=condition=Ready", "-l", "!job-name", "-n", ns))
}

// waitForWebhookReady waits for the Knative Serving webhook to be ready.
Expand Down
70 changes: 70 additions & 0 deletions pkg/install/install_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright 漏 2026 The Knative Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package install

import "testing"

func TestParseKubectlVersion(t *testing.T) {
tests := []struct {
name string
json string
wantMajor int
wantMinor int
wantErr bool
}{{
name: "standard release",
json: `{"clientVersion":{"major":"1","minor":"34","gitVersion":"v1.34.0"}}`,
wantMajor: 1,
wantMinor: 34,
}, {
name: "patch version",
json: `{"clientVersion":{"major":"1","minor":"33","gitVersion":"v1.33.2"}}`,
wantMajor: 1,
wantMinor: 33,
}, {
name: "vendor suffix in gitVersion",
// EKS and other distributions append a suffix; we must read the
// numeric prefix, not choke on it.
json: `{"clientVersion":{"major":"1","minor":"33+","gitVersion":"v1.33.2-eks-1234567"}}`,
wantMajor: 1,
wantMinor: 33,
}, {
name: "no version present",
json: `{"clientVersion":{"buildDate":"2026-01-01"}}`,
wantErr: true,
}, {
name: "garbage",
json: `not json at all`,
wantErr: true,
}}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
major, minor, err := parseKubectlVersion(tt.json)
if tt.wantErr {
if err == nil {
t.Fatalf("expected error, got major=%d minor=%d", major, minor)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if major != tt.wantMajor || minor != tt.wantMinor {
t.Errorf("got v%d.%d, want v%d.%d", major, minor, tt.wantMajor, tt.wantMinor)
}
})
}
}
3 changes: 3 additions & 0 deletions pkg/kind/kind.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ func createKindCluster(registry bool, extraMountHostPath string, extraMountConta
if err := checkKindVersion(); err != nil {
return fmt.Errorf("unable to check kind version: %w", err)
}
if err := install.CheckKubectlVersion(); err != nil {
return fmt.Errorf("unable to check kubectl version: %w", err)
}
if registry {
fmt.Println("馃捊 Installing local registry...")
if err := pullLocalRegistryImage(dcli); err != nil {
Expand Down
3 changes: 3 additions & 0 deletions pkg/minikube/minikube.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ func createMinikubeCluster() error {
if err := checkMinikubeVersion(); err != nil {
return fmt.Errorf("unable to get minikube version: %w", err)
}
if err := install.CheckKubectlVersion(); err != nil {
return fmt.Errorf("unable to check kubectl version: %w", err)
}
if err := checkForExistingCluster(); err != nil {
return fmt.Errorf("failure while handling or checking for existing minikube cluster: %w", err)
}
Expand Down
Loading