mirror of
https://github.com/k8sgpt-ai/k8sgpt.git
synced 2025-09-10 11:39:40 +00:00
feat: add ClusterCatalog and ClusterExtension analyzers (#1555)
Signed-off-by: Jian Zhang <jiazha@redhat.com>
This commit is contained in:
@@ -270,6 +270,8 @@ you will be able to write your own analyzers.
|
|||||||
- [x] logAnalyzer
|
- [x] logAnalyzer
|
||||||
- [x] storageAnalyzer
|
- [x] storageAnalyzer
|
||||||
- [x] securityAnalyzer
|
- [x] securityAnalyzer
|
||||||
|
- [x] ClusterCatalog
|
||||||
|
- [x] ClusterExtension
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@@ -57,6 +57,8 @@ var additionalAnalyzerMap = map[string]common.IAnalyzer{
|
|||||||
"HTTPRoute": HTTPRouteAnalyzer{},
|
"HTTPRoute": HTTPRouteAnalyzer{},
|
||||||
"Storage": StorageAnalyzer{},
|
"Storage": StorageAnalyzer{},
|
||||||
"Security": SecurityAnalyzer{},
|
"Security": SecurityAnalyzer{},
|
||||||
|
"ClusterCatalog": ClusterCatalogAnalyzer{},
|
||||||
|
"ClusterExtension": ClusterExtensionAnalyzer{},
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListFilters() ([]string, []string, []string) {
|
func ListFilters() ([]string, []string, []string) {
|
||||||
|
161
pkg/analyzer/clustercatalog.go
Normal file
161
pkg/analyzer/clustercatalog.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||||
|
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClusterCatalogAnalyzer struct{}
|
||||||
|
|
||||||
|
func (ClusterCatalogAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||||
|
|
||||||
|
kind := "ClusterCatalog"
|
||||||
|
|
||||||
|
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||||
|
"analyzer_name": kind,
|
||||||
|
})
|
||||||
|
|
||||||
|
var clusterCatalogGVR = schema.GroupVersionResource{
|
||||||
|
Group: "olm.operatorframework.io",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "clustercatalogs",
|
||||||
|
}
|
||||||
|
if a.Client == nil {
|
||||||
|
return nil, fmt.Errorf("client is nil in ClusterCatalogAnalyzer")
|
||||||
|
}
|
||||||
|
if a.Client.GetDynamicClient() == nil {
|
||||||
|
return nil, fmt.Errorf("dynamic client is nil in ClusterCatalogAnalyzer")
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := a.Client.GetDynamicClient().Resource(clusterCatalogGVR).Namespace("").List(a.Context, metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var preAnalysis = map[string]common.PreAnalysis{}
|
||||||
|
|
||||||
|
for _, item := range list.Items {
|
||||||
|
var failures []common.Failure
|
||||||
|
catalog, err := ConvertToClusterCatalog(&item)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Printf("ClusterCatalog: %s | Source: %s\n", catalog.Name, catalog.Spec.Source.Image.Ref)
|
||||||
|
failures, err = ValidateClusterCatalog(failures, catalog)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(failures) > 0 {
|
||||||
|
preAnalysis[catalog.Name] = common.PreAnalysis{
|
||||||
|
Catalog: *catalog,
|
||||||
|
FailureDetails: failures,
|
||||||
|
}
|
||||||
|
AnalyzerErrorsMetric.WithLabelValues(kind, catalog.Name, "").Set(float64(len(failures)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range preAnalysis {
|
||||||
|
var currentAnalysis = common.Result{
|
||||||
|
Kind: kind,
|
||||||
|
Name: key,
|
||||||
|
Error: value.FailureDetails,
|
||||||
|
}
|
||||||
|
|
||||||
|
parent, found := util.GetParent(a.Client, value.Node.ObjectMeta)
|
||||||
|
if found {
|
||||||
|
currentAnalysis.ParentObject = parent
|
||||||
|
}
|
||||||
|
a.Results = append(a.Results, currentAnalysis)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.Results, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConvertToClusterCatalog(u *unstructured.Unstructured) (*common.ClusterCatalog, error) {
|
||||||
|
var cc common.ClusterCatalog
|
||||||
|
err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &cc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert to ClusterCatalog: %w", err)
|
||||||
|
}
|
||||||
|
return &cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addCatalogConditionFailure(failures []common.Failure, catalogName string, catalogCondition metav1.Condition) []common.Failure {
|
||||||
|
failures = append(failures, common.Failure{
|
||||||
|
Text: fmt.Sprintf("OLMv1 ClusterCatalog: %s has condition of type %s, reason %s: %s", catalogName, catalogCondition.Type, catalogCondition.Reason, catalogCondition.Message),
|
||||||
|
Sensitive: []common.Sensitive{
|
||||||
|
{
|
||||||
|
Unmasked: catalogName,
|
||||||
|
Masked: util.MaskString(catalogName),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return failures
|
||||||
|
}
|
||||||
|
|
||||||
|
func addCatalogFailure(failures []common.Failure, catalogName string, err error) []common.Failure {
|
||||||
|
failures = append(failures, common.Failure{
|
||||||
|
Text: fmt.Sprintf("%s has error: %s", catalogName, err.Error()),
|
||||||
|
Sensitive: []common.Sensitive{
|
||||||
|
{
|
||||||
|
Unmasked: catalogName,
|
||||||
|
Masked: util.MaskString(catalogName),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return failures
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateClusterCatalog(failures []common.Failure, catalog *common.ClusterCatalog) ([]common.Failure, error) {
|
||||||
|
if !isValidImageRef(catalog.Spec.Source.Image.Ref) {
|
||||||
|
failures = addCatalogFailure(failures, catalog.Name, fmt.Errorf("invalid image ref format in spec.source.image.ref: %s", catalog.Spec.Source.Image.Ref))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check status.resolvedSource.image.ref ends with @sha256:...
|
||||||
|
if catalog.Status.ResolvedSource != nil {
|
||||||
|
if catalog.Status.ResolvedSource.Image.Ref == "" {
|
||||||
|
failures = addCatalogFailure(failures, catalog.Name, fmt.Errorf("missing status.resolvedSource.image.ref"))
|
||||||
|
}
|
||||||
|
if !regexp.MustCompile(`@sha256:[a-f0-9]{64}$`).MatchString(catalog.Status.ResolvedSource.Image.Ref) {
|
||||||
|
failures = addCatalogFailure(failures, catalog.Name, fmt.Errorf("status.resolvedSource.image.ref must end with @sha256:<digest>"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, condition := range catalog.Status.Conditions {
|
||||||
|
if condition.Status != "True" && condition.Type == "Serving" {
|
||||||
|
failures = addCatalogConditionFailure(failures, catalog.Name, condition)
|
||||||
|
}
|
||||||
|
if condition.Type == "Progressing" && condition.Reason != "Succeeded" {
|
||||||
|
failures = addCatalogConditionFailure(failures, catalog.Name, condition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return failures, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidImageRef does a simple regex check to validate image refs
|
||||||
|
func isValidImageRef(ref string) bool {
|
||||||
|
pattern := `^([a-zA-Z0-9\-\.]+(?::[0-9]+)?/)?([a-z0-9]+(?:[._\-\/][a-z0-9]+)*)(:[\w][\w.-]{0,127})?(?:@sha256:[a-f0-9]{64})?$`
|
||||||
|
return regexp.MustCompile(pattern).MatchString(ref)
|
||||||
|
}
|
182
pkg/analyzer/clustercatalog_test.go
Normal file
182
pkg/analyzer/clustercatalog_test.go
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||||
|
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
dynamicfake "k8s.io/client-go/dynamic/fake"
|
||||||
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClusterCatalogAnalyzer(t *testing.T) {
|
||||||
|
gvr := schema.GroupVersionResource{
|
||||||
|
Group: "olm.operatorframework.io",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "clustercatalogs",
|
||||||
|
}
|
||||||
|
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
|
||||||
|
dynamicClient := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(
|
||||||
|
scheme,
|
||||||
|
map[schema.GroupVersionResource]string{
|
||||||
|
gvr: "ClusterCatalogList",
|
||||||
|
},
|
||||||
|
&unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "olm.operatorframework.io/v1",
|
||||||
|
"kind": "ClusterCatalog",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "Valid ClusterCatalog",
|
||||||
|
},
|
||||||
|
"spec": map[string]interface{}{
|
||||||
|
"availabilityMode": "Available",
|
||||||
|
"source": map[string]interface{}{
|
||||||
|
"type": "Image",
|
||||||
|
"image": map[string]interface{}{
|
||||||
|
"ref": "registry.redhat.io/redhat/community-operator-index:v4.19",
|
||||||
|
"pollIntervalMinutes": float64(10),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"status": map[string]interface{}{
|
||||||
|
"conditions": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"type": "Progressing",
|
||||||
|
"status": "True",
|
||||||
|
"reason": "Succeeded",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"type": "Serving",
|
||||||
|
"status": "True",
|
||||||
|
"reason": "Available",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "olm.operatorframework.io/v1",
|
||||||
|
"kind": "ClusterCatalog",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "Invalid availabilityMode",
|
||||||
|
},
|
||||||
|
"spec": map[string]interface{}{
|
||||||
|
"availabilityMode": "test",
|
||||||
|
"source": map[string]interface{}{
|
||||||
|
"type": "Image",
|
||||||
|
"image": map[string]interface{}{
|
||||||
|
"ref": "registry.redhat.io/redhat/community-operator-index:v4.19",
|
||||||
|
"pollIntervalMinutes": float64(10),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"status": map[string]interface{}{
|
||||||
|
"conditions": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"type": "Progressing",
|
||||||
|
"status": "True",
|
||||||
|
"reason": "Retrying",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "olm.operatorframework.io/v1",
|
||||||
|
"kind": "ClusterCatalog",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "Invalid pollIntervalMinutes",
|
||||||
|
},
|
||||||
|
"spec": map[string]interface{}{
|
||||||
|
"availabilityMode": "Available",
|
||||||
|
"source": map[string]interface{}{
|
||||||
|
"type": "Image",
|
||||||
|
"image": map[string]interface{}{
|
||||||
|
"ref": "registry.redhat.io/redhat/community-operator-index:v4.19",
|
||||||
|
"pollIntervalMinutes": float64(0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"status": map[string]interface{}{
|
||||||
|
"conditions": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"type": "Progressing",
|
||||||
|
"status": "True",
|
||||||
|
"reason": "Retrying",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "olm.operatorframework.io/v1",
|
||||||
|
"kind": "ClusterCatalog",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "Invalid image reference",
|
||||||
|
},
|
||||||
|
"spec": map[string]interface{}{
|
||||||
|
"availabilityMode": "Available",
|
||||||
|
"source": map[string]interface{}{
|
||||||
|
"type": "Image",
|
||||||
|
"image": map[string]interface{}{
|
||||||
|
"ref": "quay.io/test/community-operator-index:v4.19",
|
||||||
|
"pollIntervalMinutes": float64(10),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"status": map[string]interface{}{
|
||||||
|
"conditions": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"type": "Progressing",
|
||||||
|
"status": "True",
|
||||||
|
"reason": "Retrying",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
config := common.Analyzer{
|
||||||
|
Client: &kubernetes.Client{
|
||||||
|
Client: fake.NewSimpleClientset(),
|
||||||
|
DynamicClient: dynamicClient,
|
||||||
|
},
|
||||||
|
Context: context.Background(),
|
||||||
|
Namespace: "test",
|
||||||
|
}
|
||||||
|
|
||||||
|
ccAnalyzer := ClusterCatalogAnalyzer{}
|
||||||
|
results, err := ccAnalyzer.Analyze(config)
|
||||||
|
for _, res := range results {
|
||||||
|
fmt.Printf("Result: %s | Failures: %d\n", res.Name, len(res.Error))
|
||||||
|
for _, err := range res.Error {
|
||||||
|
fmt.Printf(" - %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 3, len(results))
|
||||||
|
}
|
148
pkg/analyzer/clusterextension.go
Normal file
148
pkg/analyzer/clusterextension.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
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/util"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClusterExtensionAnalyzer struct{}
|
||||||
|
|
||||||
|
func (ClusterExtensionAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||||
|
|
||||||
|
kind := "ClusterExtension"
|
||||||
|
|
||||||
|
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||||
|
"analyzer_name": kind,
|
||||||
|
})
|
||||||
|
|
||||||
|
var clusterExtensionGVR = schema.GroupVersionResource{
|
||||||
|
Group: "olm.operatorframework.io",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "clusterextensions",
|
||||||
|
}
|
||||||
|
if a.Client == nil {
|
||||||
|
return nil, fmt.Errorf("client is nil in ClusterExtensionAnalyzer")
|
||||||
|
}
|
||||||
|
if a.Client.GetDynamicClient() == nil {
|
||||||
|
return nil, fmt.Errorf("dynamic client is nil in ClusterExtensionAnalyzer")
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := a.Client.GetDynamicClient().Resource(clusterExtensionGVR).Namespace("").List(a.Context, metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var preAnalysis = map[string]common.PreAnalysis{}
|
||||||
|
|
||||||
|
for _, item := range list.Items {
|
||||||
|
var failures []common.Failure
|
||||||
|
extension, err := ConvertToClusterExtension(&item)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Printf("ClusterExtension: %s | Source: %s\n", extension.Name, extension.Spec.Source.Catalog.PackageName)
|
||||||
|
failures, err = ValidateClusterExtension(failures, extension)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(failures) > 0 {
|
||||||
|
preAnalysis[extension.Name] = common.PreAnalysis{
|
||||||
|
Extension: *extension,
|
||||||
|
FailureDetails: failures,
|
||||||
|
}
|
||||||
|
AnalyzerErrorsMetric.WithLabelValues(kind, extension.Name, "").Set(float64(len(failures)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range preAnalysis {
|
||||||
|
var currentAnalysis = common.Result{
|
||||||
|
Kind: kind,
|
||||||
|
Name: key,
|
||||||
|
Error: value.FailureDetails,
|
||||||
|
}
|
||||||
|
|
||||||
|
parent, found := util.GetParent(a.Client, value.Node.ObjectMeta)
|
||||||
|
if found {
|
||||||
|
currentAnalysis.ParentObject = parent
|
||||||
|
}
|
||||||
|
a.Results = append(a.Results, currentAnalysis)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.Results, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConvertToClusterExtension(u *unstructured.Unstructured) (*common.ClusterExtension, error) {
|
||||||
|
var ce common.ClusterExtension
|
||||||
|
err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &ce)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert to ClusterExtension: %w", err)
|
||||||
|
}
|
||||||
|
return &ce, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addExtensionConditionFailure(failures []common.Failure, extensionName string, extensionCondition metav1.Condition) []common.Failure {
|
||||||
|
failures = append(failures, common.Failure{
|
||||||
|
Text: fmt.Sprintf("OLMv1 ClusterExtension: %s has condition of type %s, reason %s: %s", extensionName, extensionCondition.Type, extensionCondition.Reason, extensionCondition.Message),
|
||||||
|
Sensitive: []common.Sensitive{
|
||||||
|
{
|
||||||
|
Unmasked: extensionName,
|
||||||
|
Masked: util.MaskString(extensionName),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return failures
|
||||||
|
}
|
||||||
|
|
||||||
|
func addExtensionFailure(failures []common.Failure, extensionName string, err error) []common.Failure {
|
||||||
|
failures = append(failures, common.Failure{
|
||||||
|
Text: fmt.Sprintf("%s has error: %s", extensionName, err.Error()),
|
||||||
|
Sensitive: []common.Sensitive{
|
||||||
|
{
|
||||||
|
Unmasked: extensionName,
|
||||||
|
Masked: util.MaskString(extensionName),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return failures
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateClusterExtension(failures []common.Failure, extension *common.ClusterExtension) ([]common.Failure, error) {
|
||||||
|
if extension.Spec.Source.Catalog != nil && extension.Spec.Source.Catalog.UpgradeConstraintPolicy != "CatalogProvided" && extension.Spec.Source.Catalog.UpgradeConstraintPolicy != "SelfCertified" {
|
||||||
|
failures = addExtensionFailure(failures, extension.Name, fmt.Errorf("invalid or missing extension.Spec.Source.Catalog.UpgradeConstraintPolicy (expecting 'SelfCertified' or 'CatalogProvided')"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if extension.Spec.Source.SourceType != "Catalog" {
|
||||||
|
failures = addExtensionFailure(failures, extension.Name, fmt.Errorf("invalid or missing spec.source.sourceType (expecting 'Catalog')"))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, condition := range extension.Status.Conditions {
|
||||||
|
if condition.Status != "True" && condition.Type == "Installed" {
|
||||||
|
failures = addExtensionConditionFailure(failures, extension.Name, condition)
|
||||||
|
}
|
||||||
|
if condition.Type == "Progressing" && condition.Reason != "Succeeded" {
|
||||||
|
failures = addExtensionConditionFailure(failures, extension.Name, condition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return failures, nil
|
||||||
|
}
|
179
pkg/analyzer/clusterextension_test.go
Normal file
179
pkg/analyzer/clusterextension_test.go
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||||
|
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
dynamicfake "k8s.io/client-go/dynamic/fake"
|
||||||
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClusterExtensionAnalyzer(t *testing.T) {
|
||||||
|
gvr := schema.GroupVersionResource{
|
||||||
|
Group: "olm.operatorframework.io",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "clusterextensions",
|
||||||
|
}
|
||||||
|
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
|
||||||
|
dynamicClient := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(
|
||||||
|
scheme,
|
||||||
|
map[schema.GroupVersionResource]string{
|
||||||
|
gvr: "ClusterExtensionList",
|
||||||
|
},
|
||||||
|
&unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "olm.operatorframework.io/v1",
|
||||||
|
"kind": "ClusterExtension",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "Valid SelfCertified ClusterExtension",
|
||||||
|
},
|
||||||
|
"spec": map[string]interface{}{
|
||||||
|
"source": map[string]interface{}{
|
||||||
|
"sourceType": "Catalog",
|
||||||
|
"catalog": map[string]interface{}{
|
||||||
|
"upgradeConstraintPolicy": "SelfCertified",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"status": map[string]interface{}{
|
||||||
|
"conditions": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"type": "Installed",
|
||||||
|
"status": "True",
|
||||||
|
"reason": "Succeeded",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "olm.operatorframework.io/v1",
|
||||||
|
"kind": "ClusterExtension",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "Valid CatalogProvided ClusterExtension",
|
||||||
|
},
|
||||||
|
"spec": map[string]interface{}{
|
||||||
|
"source": map[string]interface{}{
|
||||||
|
"sourceType": "Catalog",
|
||||||
|
"catalog": map[string]interface{}{
|
||||||
|
"upgradeConstraintPolicy": "CatalogProvided",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"status": map[string]interface{}{
|
||||||
|
"conditions": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"type": "Installed",
|
||||||
|
"status": "True",
|
||||||
|
"reason": "Succeeded",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "olm.operatorframework.io/v1",
|
||||||
|
"kind": "ClusterExtension",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "Invalid UpgradeConstraintPolicy",
|
||||||
|
},
|
||||||
|
"spec": map[string]interface{}{
|
||||||
|
"source": map[string]interface{}{
|
||||||
|
"sourceType": "Catalog",
|
||||||
|
"catalog": map[string]interface{}{
|
||||||
|
"upgradeConstraintPolicy": "InvalidPolicy",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"status": map[string]interface{}{
|
||||||
|
"conditions": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"type": "Progressing",
|
||||||
|
"status": "True",
|
||||||
|
"reason": "Retrying",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"type": "Installed",
|
||||||
|
"status": "False",
|
||||||
|
"reason": "Failed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "olm.operatorframework.io/v1",
|
||||||
|
"kind": "ClusterExtension",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "Invalid SourceType",
|
||||||
|
},
|
||||||
|
"spec": map[string]interface{}{
|
||||||
|
"source": map[string]interface{}{
|
||||||
|
"sourceType": "Git",
|
||||||
|
"catalog": map[string]interface{}{
|
||||||
|
"upgradeConstraintPolicy": "CatalogProvided",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"status": map[string]interface{}{
|
||||||
|
"conditions": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"type": "Progressing",
|
||||||
|
"status": "True",
|
||||||
|
"reason": "Retrying",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"type": "Installed",
|
||||||
|
"status": "False",
|
||||||
|
"reason": "Failed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
config := common.Analyzer{
|
||||||
|
Client: &kubernetes.Client{
|
||||||
|
Client: fake.NewSimpleClientset(),
|
||||||
|
DynamicClient: dynamicClient,
|
||||||
|
},
|
||||||
|
Context: context.Background(),
|
||||||
|
Namespace: "test",
|
||||||
|
}
|
||||||
|
|
||||||
|
ceAnalyzer := ClusterExtensionAnalyzer{}
|
||||||
|
results, err := ceAnalyzer.Analyze(config)
|
||||||
|
for _, res := range results {
|
||||||
|
fmt.Printf("Result: %s | Failures: %d\n", res.Name, len(res.Error))
|
||||||
|
for _, err := range res.Error {
|
||||||
|
fmt.Printf(" - %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(results))
|
||||||
|
}
|
@@ -28,6 +28,7 @@ import (
|
|||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
networkv1 "k8s.io/api/networking/v1"
|
networkv1 "k8s.io/api/networking/v1"
|
||||||
policyv1 "k8s.io/api/policy/v1"
|
policyv1 "k8s.io/api/policy/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
gtwapi "sigs.k8s.io/gateway-api/apis/v1"
|
gtwapi "sigs.k8s.io/gateway-api/apis/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -68,6 +69,8 @@ type PreAnalysis struct {
|
|||||||
ScaledObject keda.ScaledObject
|
ScaledObject keda.ScaledObject
|
||||||
KyvernoPolicyReport kyverno.PolicyReport
|
KyvernoPolicyReport kyverno.PolicyReport
|
||||||
KyvernoClusterPolicyReport kyverno.ClusterPolicyReport
|
KyvernoClusterPolicyReport kyverno.ClusterPolicyReport
|
||||||
|
Catalog ClusterCatalog
|
||||||
|
Extension ClusterExtension
|
||||||
}
|
}
|
||||||
|
|
||||||
type Result struct {
|
type Result struct {
|
||||||
@@ -93,3 +96,117 @@ type Sensitive struct {
|
|||||||
Unmasked string
|
Unmasked string
|
||||||
Masked string
|
Masked string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
SourceType string
|
||||||
|
AvailabilityMode string
|
||||||
|
UpgradeConstraintPolicy string
|
||||||
|
CRDUpgradeSafetyEnforcement string
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClusterCatalog struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ObjectMeta `json:"metadata"`
|
||||||
|
Spec ClusterCatalogSpec `json:"spec"`
|
||||||
|
Status ClusterCatalogStatus `json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
type ClusterCatalogList struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ListMeta `json:"metadata"`
|
||||||
|
Items []ClusterCatalog `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClusterCatalogSpec struct {
|
||||||
|
Source CatalogSource `json:"source"`
|
||||||
|
|
||||||
|
Priority int32 `json:"priority"`
|
||||||
|
|
||||||
|
AvailabilityMode AvailabilityMode `json:"availabilityMode,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClusterCatalogStatus struct {
|
||||||
|
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
|
||||||
|
|
||||||
|
ResolvedSource *ResolvedCatalogSource `json:"resolvedSource,omitempty"`
|
||||||
|
|
||||||
|
URLs *ClusterCatalogURLs `json:"urls,omitempty"`
|
||||||
|
LastUnpacked *metav1.Time `json:"lastUnpacked,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClusterCatalogURLs struct {
|
||||||
|
Base string `json:"base"`
|
||||||
|
}
|
||||||
|
type CatalogSource struct {
|
||||||
|
Type SourceType `json:"type"`
|
||||||
|
Image *ImageSource `json:"image,omitempty"`
|
||||||
|
}
|
||||||
|
type ResolvedCatalogSource struct {
|
||||||
|
Type SourceType `json:"type"`
|
||||||
|
Image *ResolvedImageSource `json:"image"`
|
||||||
|
}
|
||||||
|
type ResolvedImageSource struct {
|
||||||
|
Ref string `json:"ref"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageSource struct {
|
||||||
|
Ref string `json:"ref"`
|
||||||
|
PollIntervalMinutes *int `json:"pollIntervalMinutes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClusterExtension struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||||
|
Spec ClusterExtensionSpec `json:"spec,omitempty"`
|
||||||
|
Status ClusterExtensionStatus `json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClusterExtensionSpec struct {
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
ServiceAccount ServiceAccountReference `json:"serviceAccount"`
|
||||||
|
Source SourceConfig `json:"source"`
|
||||||
|
Install *ClusterExtensionInstallConfig `json:"install,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClusterExtensionInstallConfig struct {
|
||||||
|
Preflight *PreflightConfig `json:"preflight,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PreflightConfig struct {
|
||||||
|
CRDUpgradeSafety *CRDUpgradeSafetyPreflightConfig `json:"crdUpgradeSafety"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CRDUpgradeSafetyPreflightConfig struct {
|
||||||
|
Enforcement CRDUpgradeSafetyEnforcement `json:"enforcement"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceAccountReference struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SourceConfig struct {
|
||||||
|
SourceType string `json:"sourceType"`
|
||||||
|
Catalog *CatalogFilter `json:"catalog,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CatalogFilter struct {
|
||||||
|
PackageName string `json:"packageName"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
Channels []string `json:"channels,omitempty"`
|
||||||
|
Selector *metav1.LabelSelector `json:"selector,omitempty"`
|
||||||
|
UpgradeConstraintPolicy UpgradeConstraintPolicy `json:"upgradeConstraintPolicy,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClusterExtensionStatus struct {
|
||||||
|
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
|
||||||
|
|
||||||
|
Install *ClusterExtensionInstallStatus `json:"install,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClusterExtensionInstallStatus struct {
|
||||||
|
Bundle BundleMetadata `json:"bundle"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BundleMetadata struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
@@ -14,6 +14,7 @@ limitations under the License.
|
|||||||
package kubernetes
|
package kubernetes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
|
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
@@ -33,6 +34,10 @@ func (c *Client) GetCtrlClient() ctrl.Client {
|
|||||||
return c.CtrlClient
|
return c.CtrlClient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetDynamicClient() dynamic.Interface {
|
||||||
|
return c.DynamicClient
|
||||||
|
}
|
||||||
|
|
||||||
func NewClient(kubecontext string, kubeconfig string) (*Client, error) {
|
func NewClient(kubecontext string, kubeconfig string) (*Client, error) {
|
||||||
var config *rest.Config
|
var config *rest.Config
|
||||||
config, err := rest.InClusterConfig()
|
config, err := rest.InClusterConfig()
|
||||||
@@ -69,10 +74,16 @@ func NewClient(kubecontext string, kubeconfig string) (*Client, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dynamicClient, err := dynamic.NewForConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
Client: clientSet,
|
Client: clientSet,
|
||||||
CtrlClient: ctrlClient,
|
CtrlClient: ctrlClient,
|
||||||
Config: config,
|
Config: config,
|
||||||
ServerVersion: serverVersion,
|
ServerVersion: serverVersion,
|
||||||
|
DynamicClient: dynamicClient,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import (
|
|||||||
openapi_v2 "github.com/google/gnostic/openapiv2"
|
openapi_v2 "github.com/google/gnostic/openapiv2"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/version"
|
"k8s.io/apimachinery/pkg/version"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
ctrl "sigs.k8s.io/controller-runtime/pkg/client"
|
ctrl "sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
@@ -14,6 +15,7 @@ type Client struct {
|
|||||||
CtrlClient ctrl.Client
|
CtrlClient ctrl.Client
|
||||||
Config *rest.Config
|
Config *rest.Config
|
||||||
ServerVersion *version.Info
|
ServerVersion *version.Info
|
||||||
|
DynamicClient dynamic.Interface
|
||||||
}
|
}
|
||||||
|
|
||||||
type K8sApiReference struct {
|
type K8sApiReference struct {
|
||||||
|
Reference in New Issue
Block a user