Skip to content

Commit

Permalink
feat: add custom resource definition(CRD) analyzer and validate conve…
Browse files Browse the repository at this point in the history
…rsion webhook

Signed-off-by: Samantha Jayasinghe <[email protected]>
  • Loading branch information
samanthajayasinghe committed Mar 25, 2024
1 parent bd2e06b commit 553f135
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 1 deletion.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ require (
buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.3.0-20240213144542-6e830f3fdf19.2
buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.32.0-20240213144542-6e830f3fdf19.1
cloud.google.com/go/storage v1.38.0
cloud.google.com/go/vertexai v0.7.1
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.1
github.com/aws/aws-sdk-go v1.50.34
Expand Down Expand Up @@ -55,7 +56,6 @@ require (
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.6 // indirect
cloud.google.com/go/longrunning v0.5.5 // indirect
cloud.google.com/go/vertexai v0.7.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 // indirect
Expand Down
1 change: 1 addition & 0 deletions pkg/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ var additionalAnalyzerMap = map[string]common.IAnalyzer{
"GatewayClass": GatewayClassAnalyzer{},
"Gateway": GatewayAnalyzer{},
"HTTPRoute": HTTPRouteAnalyzer{},
"Crd": CrdAnalyzer{},
}

func ListFilters() ([]string, []string, []string) {
Expand Down
122 changes: 122 additions & 0 deletions pkg/analyzer/crd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
Copyright 2023 The K8sGPT 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 analyzer

import (
"fmt"

"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
ctrl "sigs.k8s.io/controller-runtime/pkg/client"
)

const (
kind = "CustomResourceDefinition"
webhook = "Webhook"
serviceNotFound = "Custom Resource Definition Conversion Webhook Service %s not found"
apiSpecWebhookService = "spec.conversion.webhook.clientConfig.service"
)

type CrdAnalyzer struct {
}

func (CrdAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {

apiDoc := kubernetes.K8sApiReference{
Kind: kind,
ApiVersion: schema.GroupVersion{
Group: "apps",
Version: "v1",
},
OpenapiSchema: a.OpenapiSchema,
}

AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
"analyzer_name": kind,
})

var preAnalysis = map[string]common.PreAnalysis{}

// Fetch all CRD's
client := a.Client.CtrlClient
crdList := &apiextensionsv1.CustomResourceDefinitionList{}
client.List(a.Context, &apiextensionsv1.CustomResourceDefinitionList{})
if err := client.List(a.Context, crdList, &ctrl.ListOptions{}); err != nil {
return nil, err
}

var failures []common.Failure

for _, crd := range crdList.Items {

// Check crd conversion webhook service
conversion := crd.Spec.Conversion
if conversion.Strategy == webhook && conversion.Webhook.ClientConfig.Service != nil {

svc := crd.Spec.Conversion.Webhook.ClientConfig.Service
// Get the webhook service
_, err := a.Client.GetClient().
CoreV1().
Services(svc.Namespace).
Get(a.Context, svc.Name, v1.GetOptions{})
if err != nil {
// If the service is not found, can't create the custom resource
failures = append(failures, common.Failure{
Text: fmt.Sprintf(serviceNotFound, svc.Name),
KubernetesDoc: apiDoc.GetApiDocV2(apiSpecWebhookService),
Sensitive: []common.Sensitive{
{
Unmasked: svc.Namespace,
Masked: util.MaskString(svc.Namespace),
},
{
Unmasked: svc.Name,
Masked: util.MaskString(svc.Name),
},
},
})

AnalyzerErrorsMetric.WithLabelValues(
crd.Spec.Names.Singular,
svc.Name,
svc.Namespace,
).Set(float64(len(failures)))
}

}

if len(failures) > 0 {
preAnalysis[fmt.Sprintf("%s", crd.Name)] = common.PreAnalysis{
FailureDetails: failures,
}

}
}

for key, value := range preAnalysis {
var currentAnalysis = common.Result{
Kind: kind,
Name: key,
Error: value.FailureDetails,
}

a.Results = append(a.Results, currentAnalysis)
}

return a.Results, nil
}
142 changes: 142 additions & 0 deletions pkg/analyzer/crd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
Copyright 2023 The K8sGPT 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 analyzer

import (
"context"
"testing"

"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/magiconair/properties/assert"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/kubernetes/scheme"
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestCRDSuccess(t *testing.T) {
crd := apiextensionsv1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "crdTest.stable.example.com",
},
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
Group: "stable.example.com",
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{{
Name: "v1alpha1",
Served: true,
Storage: true,
}},
Names: apiextensionsv1.CustomResourceDefinitionNames{
Plural: "crdtests",
Singular: "crdtest",
Kind: "CrdTest",
},
Scope: apiextensionsv1.ClusterScoped,
},
}

scheme := scheme.Scheme

err := apiextensionsv1.AddToScheme(scheme)
if err != nil {
t.Error(err)
}

objects := []runtime.Object{
&crd,
}

fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build()

analyzerInstance := CrdAnalyzer{}
config := common.Analyzer{
Client: &kubernetes.Client{
CtrlClient: fakeClient,
Config: nil,
},
Context: context.Background(),
Namespace: "default",
}
analysisResults, err := analyzerInstance.Analyze(config)
if err != nil {
t.Error(err)
}
assert.Equal(t, len(analysisResults), 0)
}

func TestCRDFailForConverstionWebhook(t *testing.T) {
crd := apiextensionsv1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "crdTest.stable.example.com",
},
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
Group: "stable.example.com",
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{{
Name: "v1alpha1",
Served: true,
Storage: true,
}},
Names: apiextensionsv1.CustomResourceDefinitionNames{
Plural: "crdtests",
Singular: "crdtest",
Kind: "CrdTest",
},
Scope: apiextensionsv1.ClusterScoped,
Conversion: &apiextensionsv1.CustomResourceConversion{
Strategy: "Webhook",
Webhook: &apiextensionsv1.WebhookConversion{
ClientConfig: &apiextensionsv1.WebhookClientConfig{
Service: &apiextensionsv1.ServiceReference{
Name: "example-conversion-webhook-server",
Namespace: "default",
},
},
},
},
},
}

scheme := scheme.Scheme

err := apiextensionsv1.AddToScheme(scheme)
if err != nil {
t.Error(err)
}

objects := []runtime.Object{
&crd,
}

fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build()

analyzerInstance := CrdAnalyzer{}
clientset := fake.NewSimpleClientset()
config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientset,
CtrlClient: fakeClient,
Config: nil,
},
Context: context.Background(),
Namespace: "default",
}
analysisResults, err := analyzerInstance.Analyze(config)
if err != nil {
t.Error(err)
}
assert.Equal(t, len(analysisResults), 1)
}

0 comments on commit 553f135

Please sign in to comment.