Skip to content

Commit 6ed4384

Browse files
authored
feat(CLI): add a check requiring the gateway API (#15409)
When Linkerd is installed with the CLI, we first check to ensure the Gateway API CRDs are installed, and abort if the Gateway API is not present. However, if Helm is used to the install Linkerd, no such check is performed. This can lead to an invalid installation because HTTPRoute is a required CRD for Linkerd. We add a check to `linkerd check` that validates that the Gateway API is installed. But note the prior art in: #12917 In that PR we _removed_ a check for the Gateway API. This is because the Gateway API consists of many different CRDs, most of which are optional for Linkerd; only HTTPRoute is required. Therefore, we don't want to require that all Gateway API CRDs are installed. For this new check, the HTTPRoute CRD alone is sufficient to make it pass. This matches the behavior of the condition checked by the `linkerd install` command. Signed-off-by: Alex Leong <alex@buoyant.io>
1 parent 782b136 commit 6ed4384

8 files changed

Lines changed: 207 additions & 77 deletions

File tree

cli/cmd/check.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,13 @@ func configureAndRunChecks(cmd *cobra.Command, wout io.Writer, werr io.Writer, o
139139
checks := []healthcheck.CategoryID{
140140
healthcheck.KubernetesAPIChecks,
141141
healthcheck.KubernetesVersionChecks,
142+
healthcheck.GatewayAPICRDChecks,
142143
healthcheck.LinkerdVersionChecks,
143144
}
144145

145146
crdManifest := bytes.Buffer{}
146147
err = renderCRDs(cmd.Context(), nil, &crdManifest, valuespkg.Options{
147-
// GatewayAPI CRDs are optional so don't check for them.
148+
// Gateway API CRDs are checked separately by GatewayAPICRDChecks.
148149
Values: []string{
149150
"installGatewayAPI=false",
150151
},

cli/cmd/install.go

Lines changed: 10 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import (
2828
valuespkg "helm.sh/helm/v3/pkg/cli/values"
2929
"helm.sh/helm/v3/pkg/engine"
3030
corev1 "k8s.io/api/core/v1"
31-
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
3231
kerrors "k8s.io/apimachinery/pkg/api/errors"
3332
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3433
"k8s.io/apimachinery/pkg/util/intstr"
@@ -206,51 +205,6 @@ func checkNoConfig(ctx context.Context, k8sAPI *k8s.KubernetesAPI) error {
206205
return nil
207206
}
208207

209-
type GatewayAPICRDs int
210-
211-
const (
212-
Absent GatewayAPICRDs = iota
213-
Linkerd
214-
External
215-
)
216-
217-
// checkGatewayAPICRDs returns true if the Gateway API CRDs are installed in the
218-
// cluster, and false otherwise.
219-
func checkGatewayAPICRDs(ctx context.Context, k8sAPI *k8s.KubernetesAPI) (GatewayAPICRDs, error) {
220-
crds := k8sAPI.Apiextensions.ApiextensionsV1().CustomResourceDefinitions()
221-
result := Absent
222-
names := []string{
223-
"httproutes.gateway.networking.k8s.io",
224-
"grpcroutes.gateway.networking.k8s.io",
225-
}
226-
for _, name := range names {
227-
crd, err := crds.Get(ctx, name, metav1.GetOptions{})
228-
if err == nil && crd != nil {
229-
if crd.Annotations[k8s.CreatedByAnnotation] != "" {
230-
return Linkerd, nil
231-
}
232-
result = External
233-
if !crdIncludesV1(crd) {
234-
return result, fmt.Errorf("the %s CRD is missing the v1 version, please upgrade to Gateway API v1.1.1 or later", name)
235-
}
236-
} else if kerrors.IsNotFound(err) {
237-
// No action if CRD is not found.
238-
} else {
239-
return Absent, err
240-
}
241-
}
242-
return result, nil
243-
}
244-
245-
func crdIncludesV1(crd *v1.CustomResourceDefinition) bool {
246-
for _, version := range crd.Spec.Versions {
247-
if version.Name == "v1" {
248-
return true
249-
}
250-
}
251-
return false
252-
}
253-
254208
func installCRDs(ctx context.Context, k8sAPI *k8s.KubernetesAPI, w io.Writer, options valuespkg.Options, format string) error {
255209
if err := checkNoConfig(ctx, k8sAPI); err != nil {
256210
return err
@@ -390,22 +344,22 @@ func renderChartToBuffer(files []*loader.BufferedFile, values map[string]interfa
390344
return &buf, vals, nil
391345
}
392346

393-
func updateDefaultValues(installed GatewayAPICRDs, defaultValues map[string]interface{}) map[string]interface{} {
394-
if installed == Absent {
347+
func updateDefaultValues(installed healthcheck.GatewayAPICRDs, defaultValues map[string]interface{}) map[string]interface{} {
348+
if installed == healthcheck.GatewayAPIAbsent {
395349
// if GW API is not installed, default to false
396350
defaultValues["installGatewayAPI"] = false
397-
} else if installed == Linkerd {
351+
} else if installed == healthcheck.GatewayAPILinkerd {
398352
// if it is installed by Linkerd, default to true
399353
defaultValues["installGatewayAPI"] = true
400-
} else if installed == External {
354+
} else if installed == healthcheck.GatewayAPIExternal {
401355
// if it is external, default to false as we are not managing it
402356
defaultValues["installGatewayAPI"] = false
403357
}
404358

405359
return defaultValues
406360
}
407361

408-
func validateFinalValues(installed GatewayAPICRDs, finalValues map[string]interface{}) error {
362+
func validateFinalValues(installed healthcheck.GatewayAPICRDs, finalValues map[string]interface{}) error {
409363
installing := false
410364

411365
if installGatewayAPI, ok := finalValues["installGatewayAPI"]; ok {
@@ -416,21 +370,17 @@ func validateFinalValues(installed GatewayAPICRDs, finalValues map[string]interf
416370
installing = enableHttpRoutes == true
417371
}
418372

419-
if installed == Absent {
373+
if installed == healthcheck.GatewayAPIAbsent {
420374
if !installing {
421375
// if we are not installing GW API Resources and they are not present, error
422-
return errors.New(`The Gateway API CRDs must be installed prior to installing Linkerd. Run:
423-
424-
kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml
425-
426-
or see https://gateway-api.sigs.k8s.io/guides/#installing-gateway-api for more options.`)
376+
return healthcheck.GatewayAPICRDsMissingError()
427377
}
428-
} else if installed == Linkerd {
378+
} else if installed == healthcheck.GatewayAPILinkerd {
429379
if !installing {
430380
// if they are installed and managed by Linkerd, we cannot uninstall them
431381
return errors.New("Linkerd is providing GW API, but your current install configuration will remove it")
432382
}
433-
} else if installed == External {
383+
} else if installed == healthcheck.GatewayAPIExternal {
434384
if installing {
435385
// if they are installed but are external, we cannot be installing as well
436386
return errors.New("Linkerd cannot install the Gateway API CRDs because they are already installed by an external source. Please set `installGatewayAPI` to `false`.")
@@ -476,7 +426,7 @@ func renderCRDs(ctx context.Context, k *k8s.KubernetesAPI, w io.Writer, options
476426
// If any of the Gateway API CRDs are installed, we default to rendering the
477427
// Gateway API CRDs.
478428
if k != nil {
479-
installed, err := checkGatewayAPICRDs(ctx, k)
429+
installed, err := healthcheck.CheckGatewayAPICRDs(ctx, k)
480430
if err != nil {
481431
return err
482432
}

pkg/healthcheck/healthcheck.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ const (
5454
// requirements.
5555
KubernetesVersionChecks CategoryID = "kubernetes-version"
5656

57+
// GatewayAPICRDChecks validates that the Gateway API CRDs exist.
58+
// These checks are dependent on the output of KubernetesAPIChecks, so those
59+
// checks must be added first.
60+
GatewayAPICRDChecks CategoryID = "gateway-api-crd"
61+
5762
// LinkerdPreInstall* checks enabled by `linkerd check --pre`
5863

5964
// LinkerdPreInstallChecks adds checks to validate that the control plane
@@ -191,6 +196,14 @@ var ExpectedServiceAccountNames = []string{
191196
"linkerd-proxy-injector",
192197
}
193198

199+
type GatewayAPICRDs int
200+
201+
const (
202+
GatewayAPIAbsent GatewayAPICRDs = iota
203+
GatewayAPILinkerd
204+
GatewayAPIExternal
205+
)
206+
194207
var (
195208
retryWindow = 5 * time.Second
196209
// RequestTimeout is the time it takes for a request to timeout
@@ -551,6 +564,20 @@ func (hc *HealthChecker) allCategories() []*Category {
551564
},
552565
false,
553566
),
567+
NewCategory(
568+
GatewayAPICRDChecks,
569+
[]Checker{
570+
{
571+
description: "Gateway API CRDs are installed",
572+
hintAnchor: "gateway-api-crd",
573+
fatal: true,
574+
check: func(ctx context.Context) error {
575+
return CheckGatewayAPICRDsInstalled(ctx, hc.kubeAPI)
576+
},
577+
},
578+
},
579+
false,
580+
),
554581
NewCategory(
555582
LinkerdPreInstallChecks,
556583
[]Checker{
@@ -2201,6 +2228,54 @@ func CheckCustomResourceDefinitions(ctx context.Context, k8sAPI *k8s.KubernetesA
22012228
return nil
22022229
}
22032230

2231+
// CheckGatewayAPICRDs returns whether the Gateway API CRDs are installed in the
2232+
// cluster, and whether they were created by Linkerd or an external source.
2233+
func CheckGatewayAPICRDs(ctx context.Context, k8sAPI *k8s.KubernetesAPI) (GatewayAPICRDs, error) {
2234+
crds := k8sAPI.Apiextensions.ApiextensionsV1().CustomResourceDefinitions()
2235+
result := GatewayAPIAbsent
2236+
names := []string{
2237+
"httproutes.gateway.networking.k8s.io",
2238+
"grpcroutes.gateway.networking.k8s.io",
2239+
}
2240+
for _, name := range names {
2241+
crd, err := crds.Get(ctx, name, metav1.GetOptions{})
2242+
if err == nil && crd != nil {
2243+
if crd.Annotations[k8s.CreatedByAnnotation] != "" {
2244+
return GatewayAPILinkerd, nil
2245+
}
2246+
result = GatewayAPIExternal
2247+
if !crdIncludesV1(crd) {
2248+
return result, fmt.Errorf("the %s CRD is missing the v1 version, please upgrade to Gateway API v1.1.1 or later", name)
2249+
}
2250+
} else if kerrors.IsNotFound(err) {
2251+
// No action if CRD is not found.
2252+
} else {
2253+
return GatewayAPIAbsent, err
2254+
}
2255+
}
2256+
return result, nil
2257+
}
2258+
2259+
// CheckGatewayAPICRDsInstalled verifies that the Gateway API CRDs are installed.
2260+
func CheckGatewayAPICRDsInstalled(ctx context.Context, k8sAPI *k8s.KubernetesAPI) error {
2261+
installed, err := CheckGatewayAPICRDs(ctx, k8sAPI)
2262+
if err != nil {
2263+
return err
2264+
}
2265+
if installed == GatewayAPIAbsent {
2266+
return GatewayAPICRDsMissingError()
2267+
}
2268+
return nil
2269+
}
2270+
2271+
func GatewayAPICRDsMissingError() error {
2272+
return errors.New(`The Gateway API CRDs must be installed prior to installing Linkerd. Run:
2273+
2274+
kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml
2275+
2276+
or see https://gateway-api.sigs.k8s.io/guides/#installing-gateway-api for more options.`)
2277+
}
2278+
22042279
func crdHasVersion(crd *apiextv1.CustomResourceDefinition, version string) bool {
22052280
for _, crdVersion := range crd.Spec.Versions {
22062281
if crdVersion.Name == version {
@@ -2210,6 +2285,15 @@ func crdHasVersion(crd *apiextv1.CustomResourceDefinition, version string) bool
22102285
return false
22112286
}
22122287

2288+
func crdIncludesV1(crd *apiextv1.CustomResourceDefinition) bool {
2289+
for _, version := range crd.Spec.Versions {
2290+
if version.Name == "v1" {
2291+
return true
2292+
}
2293+
}
2294+
return false
2295+
}
2296+
22132297
// CheckNodesHaveNonDockerRuntime checks that each node has a non-Docker
22142298
// runtime. This check is only called if proxyInit is not running as root
22152299
// which is a problem for clusters with a Docker container runtime.

pkg/healthcheck/healthcheck_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,89 @@ func (hc *HealthChecker) addCheckAsCategory(
7575
hc.AppendCategories(testCategory)
7676
}
7777

78+
func TestGatewayAPICRDChecks(t *testing.T) {
79+
externalGatewayAPIManifest := `---
80+
apiVersion: apiextensions.k8s.io/v1
81+
kind: CustomResourceDefinition
82+
metadata:
83+
name: httproutes.gateway.networking.k8s.io
84+
spec:
85+
versions:
86+
- name: v1
87+
`
88+
unsupportedGatewayAPIManifest := `---
89+
apiVersion: apiextensions.k8s.io/v1
90+
kind: CustomResourceDefinition
91+
metadata:
92+
name: httproutes.gateway.networking.k8s.io
93+
spec:
94+
versions:
95+
- name: v1alpha1
96+
`
97+
98+
testCases := []struct {
99+
name string
100+
resources []string
101+
wantSuccess bool
102+
wantErr string
103+
}{
104+
{
105+
name: "passes when Gateway API CRDs are installed",
106+
resources: []string{externalGatewayAPIManifest},
107+
wantSuccess: true,
108+
},
109+
{
110+
name: "fails when Gateway API CRDs are missing",
111+
wantSuccess: false,
112+
wantErr: "The Gateway API CRDs must be installed prior to installing Linkerd",
113+
},
114+
{
115+
name: "fails when Gateway API CRDs do not include v1",
116+
resources: []string{unsupportedGatewayAPIManifest},
117+
wantSuccess: false,
118+
wantErr: "missing the v1 version",
119+
},
120+
}
121+
122+
for _, tc := range testCases {
123+
tc := tc // pin
124+
t.Run(tc.name, func(t *testing.T) {
125+
hc := NewHealthChecker(
126+
[]CategoryID{GatewayAPICRDChecks},
127+
&Options{},
128+
)
129+
130+
var err error
131+
hc.kubeAPI, err = k8s.NewFakeAPI(tc.resources...)
132+
if err != nil {
133+
t.Fatalf("Unexpected error: %s", err)
134+
}
135+
136+
var gotErr error
137+
success, _ := hc.RunChecks(func(result *CheckResult) {
138+
gotErr = result.Err
139+
})
140+
if success != tc.wantSuccess {
141+
t.Fatalf("expected success=%v, got %v", tc.wantSuccess, success)
142+
}
143+
144+
if tc.wantErr == "" {
145+
if gotErr != nil {
146+
t.Fatalf("expected no error, got %s", gotErr)
147+
}
148+
return
149+
}
150+
151+
if gotErr == nil {
152+
t.Fatalf("expected error containing %q", tc.wantErr)
153+
}
154+
if !strings.Contains(gotErr.Error(), tc.wantErr) {
155+
t.Fatalf("expected error containing %q, got %q", tc.wantErr, gotErr.Error())
156+
}
157+
})
158+
}
159+
}
160+
78161
func TestHealthChecker(t *testing.T) {
79162
nullObserver := func(*CheckResult) {}
80163

test/integration/deep/install_test.go

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,25 @@ func TestInstallCNIPlugin(t *testing.T) {
9595
testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
9696
"'kubectl apply' command failed\n%s", out)
9797
}
98+
}
99+
100+
// TestInstall will install the linkerd control plane to be used in the rest of
101+
// the deep suite tests.
102+
func TestInstall(t *testing.T) {
103+
err := TestHelper.InstallGatewayAPI()
104+
if err != nil {
105+
testutil.AnnotatedFatal(t, "failed to install gateway-api", err)
106+
}
107+
108+
// perform a linkerd check
109+
checkArgs := []string{"check", "--pre", "--wait=5m"}
110+
if TestHelper.CNI() {
111+
checkArgs = []string{"check", "--pre", "--linkerd-cni-enabled", "--wait=5m"}
112+
}
98113

99-
// perform a linkerd check with --linkerd-cni-enabled
100114
timeout := time.Minute
101115
err = testutil.RetryFor(timeout, func() error {
102-
out, err = TestHelper.LinkerdRun("check", "--pre", "--linkerd-cni-enabled", "--wait=5m")
116+
_, err = TestHelper.LinkerdRun(checkArgs...)
103117
if err != nil {
104118
return err
105119
}
@@ -108,15 +122,6 @@ func TestInstallCNIPlugin(t *testing.T) {
108122
if err != nil {
109123
testutil.AnnotatedFatal(t, fmt.Sprintf("'linkerd check' command timed-out (%s)", timeout), err)
110124
}
111-
}
112-
113-
// TestInstall will install the linkerd control plane to be used in the rest of
114-
// the deep suite tests.
115-
func TestInstall(t *testing.T) {
116-
err := TestHelper.InstallGatewayAPI()
117-
if err != nil {
118-
testutil.AnnotatedFatal(t, "failed to install gateway-api", err)
119-
}
120125

121126
// Install CRDs
122127
cmd := []string{

0 commit comments

Comments
 (0)