diff --git a/api/v1alpha1/accesskey_types.go b/api/v1alpha1/accesskey_types.go index 12b833f..440a35d 100644 --- a/api/v1alpha1/accesskey_types.go +++ b/api/v1alpha1/accesskey_types.go @@ -55,7 +55,10 @@ type AccessKeyStatus struct { // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status" -// +kubebuilder:printcolumn:name="Reason",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].reason" +// +kubebuilder:printcolumn:name="Key-ID",type="string",JSONPath=".status.accessKeyId",description="Garage access key identifier" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:printcolumn:name="Reason",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].reason",priority=1 +// +kubebuilder:printcolumn:name="Secret",type="string",JSONPath=".status.secretName",priority=1 // AccessKey is the Schema for the accesskeys API type AccessKey struct { metav1.TypeMeta `json:",inline"` diff --git a/api/v1alpha1/accesspolicy_types.go b/api/v1alpha1/accesspolicy_types.go index 1fef678..4196d6a 100644 --- a/api/v1alpha1/accesspolicy_types.go +++ b/api/v1alpha1/accesspolicy_types.go @@ -66,7 +66,13 @@ type AccessPolicyStatus struct { // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status" -// +kubebuilder:printcolumn:name="Reason",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].reason" +// +kubebuilder:printcolumn:name="Bucket",type="string",JSONPath=".spec.bucket" +// +kubebuilder:printcolumn:name="AccessKey",type="string",JSONPath=".spec.accessKey" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:printcolumn:name="Reason",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].reason",priority=1 +// +kubebuilder:printcolumn:name="Read",type="boolean",JSONPath=".spec.permissions.read",priority=1 +// +kubebuilder:printcolumn:name="Write",type="boolean",JSONPath=".spec.permissions.write",priority=1 +// +kubebuilder:printcolumn:name="Owner",type="boolean",JSONPath=".spec.permissions.owner",priority=1 // AccessPolicy is the Schema for the accesspolicies API type AccessPolicy struct { metav1.TypeMeta `json:",inline"` diff --git a/api/v1alpha1/bucket_types.go b/api/v1alpha1/bucket_types.go index bf78673..39b26a2 100644 --- a/api/v1alpha1/bucket_types.go +++ b/api/v1alpha1/bucket_types.go @@ -107,7 +107,12 @@ type BucketStatus struct { // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status" -// +kubebuilder:printcolumn:name="Reason",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].reason" +// +kubebuilder:printcolumn:name="Alias",type="string",JSONPath=".status.bucketName",description="Bucket alias as it exists in Garage" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:printcolumn:name="Reason",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].reason",priority=1 +// +kubebuilder:printcolumn:name="ID",type="string",JSONPath=".status.bucketId",description="Garage internal bucket identifier",priority=1 +// +kubebuilder:printcolumn:name="Max-Size",type="string",JSONPath=".spec.maxSize",priority=1 +// +kubebuilder:printcolumn:name="Max-Objects",type="integer",JSONPath=".spec.maxObjects",priority=1 // Bucket is the Schema for the buckets API type Bucket struct { diff --git a/config/crd/bases/garage.getclustered.net_accesskeys.yaml b/config/crd/bases/garage.getclustered.net_accesskeys.yaml index cea7754..fa94adf 100644 --- a/config/crd/bases/garage.getclustered.net_accesskeys.yaml +++ b/config/crd/bases/garage.getclustered.net_accesskeys.yaml @@ -18,8 +18,20 @@ spec: - jsonPath: .status.conditions[?(@.type=="Ready")].status name: Ready type: string + - description: Garage access key identifier + jsonPath: .status.accessKeyId + name: Key-ID + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date - jsonPath: .status.conditions[?(@.type=="Ready")].reason name: Reason + priority: 1 + type: string + - jsonPath: .status.secretName + name: Secret + priority: 1 type: string name: v1alpha1 schema: diff --git a/config/crd/bases/garage.getclustered.net_accesspolicies.yaml b/config/crd/bases/garage.getclustered.net_accesspolicies.yaml index aed48dd..66935ee 100644 --- a/config/crd/bases/garage.getclustered.net_accesspolicies.yaml +++ b/config/crd/bases/garage.getclustered.net_accesspolicies.yaml @@ -18,9 +18,31 @@ spec: - jsonPath: .status.conditions[?(@.type=="Ready")].status name: Ready type: string + - jsonPath: .spec.bucket + name: Bucket + type: string + - jsonPath: .spec.accessKey + name: AccessKey + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date - jsonPath: .status.conditions[?(@.type=="Ready")].reason name: Reason + priority: 1 type: string + - jsonPath: .spec.permissions.read + name: Read + priority: 1 + type: boolean + - jsonPath: .spec.permissions.write + name: Write + priority: 1 + type: boolean + - jsonPath: .spec.permissions.owner + name: Owner + priority: 1 + type: boolean name: v1alpha1 schema: openAPIV3Schema: diff --git a/config/crd/bases/garage.getclustered.net_buckets.yaml b/config/crd/bases/garage.getclustered.net_buckets.yaml index 1d7f78a..23ea5ba 100644 --- a/config/crd/bases/garage.getclustered.net_buckets.yaml +++ b/config/crd/bases/garage.getclustered.net_buckets.yaml @@ -18,9 +18,30 @@ spec: - jsonPath: .status.conditions[?(@.type=="Ready")].status name: Ready type: string + - description: Bucket alias as it exists in Garage + jsonPath: .status.bucketName + name: Alias + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date - jsonPath: .status.conditions[?(@.type=="Ready")].reason name: Reason + priority: 1 + type: string + - description: Garage internal bucket identifier + jsonPath: .status.bucketId + name: ID + priority: 1 + type: string + - jsonPath: .spec.maxSize + name: Max-Size + priority: 1 type: string + - jsonPath: .spec.maxObjects + name: Max-Objects + priority: 1 + type: integer name: v1alpha1 schema: openAPIV3Schema: diff --git a/internal/controller/printercolumns_test.go b/internal/controller/printercolumns_test.go new file mode 100644 index 0000000..14cb549 --- /dev/null +++ b/internal/controller/printercolumns_test.go @@ -0,0 +1,89 @@ +// Copyright 2025. +// +// 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 controller + +import ( + "encoding/json" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + + garagev1alpha1 "github.com/bmarinov/garage-storage-controller/api/v1alpha1" +) + +var _ = Describe("Printer columns", func() { + DescribeTable("the header row", + func(plural, wantPlain, wantWide string) { + gotPlain, gotWide := servedHeaders(plural) + + Expect(gotPlain).To(Equal(wantPlain)) + Expect(gotWide).To(Equal(wantWide)) + }, + Entry("kubectl get buckets", "buckets", + "NAME READY ALIAS AGE", + "NAME READY ALIAS AGE REASON ID MAX-SIZE MAX-OBJECTS"), + Entry("kubectl get accesskeys", "accesskeys", + "NAME READY KEY-ID AGE", + "NAME READY KEY-ID AGE REASON SECRET"), + Entry("kubectl get accesspolicies", "accesspolicies", + "NAME READY BUCKET ACCESSKEY AGE", + "NAME READY BUCKET ACCESSKEY AGE REASON READ WRITE OWNER"), + ) +}) + +// servedHeaders retrieves the named resource schema and renders the header row. +func servedHeaders(plural string) (plain, wide string) { + GinkgoHelper() + + // envtest installs the manifests from config/crd/bases. + // + // see suite_test.go: + // CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + + tableCfg := rest.CopyConfig(cfg) + tableCfg.GroupVersion = &garagev1alpha1.GroupVersion + tableCfg.APIPath = "/apis" + tableCfg.NegotiatedSerializer = serializer.NewCodecFactory(scheme.Scheme).WithoutConversion() + + restClient, err := rest.RESTClientFor(tableCfg) + Expect(err).NotTo(HaveOccurred()) + + raw, err := restClient.Get(). + Resource(plural). + Namespace("default"). + SetHeader("Accept", "application/json;as=Table;v=v1;g=meta.k8s.io"). + Do(ctx). + Raw() + Expect(err).NotTo(HaveOccurred()) + + var table metav1.Table + Expect(json.Unmarshal(raw, &table)).To(Succeed()) + + plainNames := make([]string, 0, len(table.ColumnDefinitions)) + wideNames := make([]string, 0, len(table.ColumnDefinitions)) + for _, c := range table.ColumnDefinitions { + wideNames = append(wideNames, strings.ToUpper(c.Name)) + if c.Priority == 0 { + plainNames = append(plainNames, strings.ToUpper(c.Name)) + } + } + return strings.Join(plainNames, " "), strings.Join(wideNames, " ") +}