diff --git a/README.md b/README.md index 31e1b32..4af860b 100644 --- a/README.md +++ b/README.md @@ -270,6 +270,8 @@ you will be able to write your own analyzers. - [x] logAnalyzer - [x] storageAnalyzer - [x] securityAnalyzer +- [x] ClusterCatalog +- [x] ClusterExtension ## Examples diff --git a/pkg/analyzer/analyzer.go b/pkg/analyzer/analyzer.go index 12c5424..ff3f809 100644 --- a/pkg/analyzer/analyzer.go +++ b/pkg/analyzer/analyzer.go @@ -57,6 +57,8 @@ var additionalAnalyzerMap = map[string]common.IAnalyzer{ "HTTPRoute": HTTPRouteAnalyzer{}, "Storage": StorageAnalyzer{}, "Security": SecurityAnalyzer{}, + "ClusterCatalog": ClusterCatalogAnalyzer{}, + "ClusterExtension": ClusterExtensionAnalyzer{}, } func ListFilters() ([]string, []string, []string) { diff --git a/pkg/analyzer/clustercatalog.go b/pkg/analyzer/clustercatalog.go new file mode 100644 index 0000000..02f0dfd --- /dev/null +++ b/pkg/analyzer/clustercatalog.go @@ -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:")) + } + } + + 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) +} diff --git a/pkg/analyzer/clustercatalog_test.go b/pkg/analyzer/clustercatalog_test.go new file mode 100644 index 0000000..918c359 --- /dev/null +++ b/pkg/analyzer/clustercatalog_test.go @@ -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)) +} diff --git a/pkg/analyzer/clusterextension.go b/pkg/analyzer/clusterextension.go new file mode 100644 index 0000000..6ba0d35 --- /dev/null +++ b/pkg/analyzer/clusterextension.go @@ -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 +} diff --git a/pkg/analyzer/clusterextension_test.go b/pkg/analyzer/clusterextension_test.go new file mode 100644 index 0000000..c684c0d --- /dev/null +++ b/pkg/analyzer/clusterextension_test.go @@ -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)) +} diff --git a/pkg/common/types.go b/pkg/common/types.go index 1070eac..69647f3 100644 --- a/pkg/common/types.go +++ b/pkg/common/types.go @@ -28,6 +28,7 @@ import ( v1 "k8s.io/api/core/v1" networkv1 "k8s.io/api/networking/v1" policyv1 "k8s.io/api/policy/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" gtwapi "sigs.k8s.io/gateway-api/apis/v1" ) @@ -68,6 +69,8 @@ type PreAnalysis struct { ScaledObject keda.ScaledObject KyvernoPolicyReport kyverno.PolicyReport KyvernoClusterPolicyReport kyverno.ClusterPolicyReport + Catalog ClusterCatalog + Extension ClusterExtension } type Result struct { @@ -93,3 +96,117 @@ type Sensitive struct { Unmasked 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"` +} diff --git a/pkg/kubernetes/kubernetes.go b/pkg/kubernetes/kubernetes.go index f3bb1d7..aa0684b 100644 --- a/pkg/kubernetes/kubernetes.go +++ b/pkg/kubernetes/kubernetes.go @@ -14,6 +14,7 @@ limitations under the License. package kubernetes import ( + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" "k8s.io/client-go/rest" @@ -33,6 +34,10 @@ func (c *Client) GetCtrlClient() ctrl.Client { return c.CtrlClient } +func (c *Client) GetDynamicClient() dynamic.Interface { + return c.DynamicClient +} + func NewClient(kubecontext string, kubeconfig string) (*Client, error) { var config *rest.Config config, err := rest.InClusterConfig() @@ -69,10 +74,16 @@ func NewClient(kubecontext string, kubeconfig string) (*Client, error) { return nil, err } + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + return nil, err + } + return &Client{ Client: clientSet, CtrlClient: ctrlClient, Config: config, ServerVersion: serverVersion, + DynamicClient: dynamicClient, }, nil } diff --git a/pkg/kubernetes/types.go b/pkg/kubernetes/types.go index 8cad275..fc4b810 100644 --- a/pkg/kubernetes/types.go +++ b/pkg/kubernetes/types.go @@ -4,6 +4,7 @@ import ( openapi_v2 "github.com/google/gnostic/openapiv2" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/version" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime/pkg/client" @@ -14,6 +15,7 @@ type Client struct { CtrlClient ctrl.Client Config *rest.Config ServerVersion *version.Info + DynamicClient dynamic.Interface } type K8sApiReference struct {