diff --git a/.dockerignore b/.dockerignore index 0f04682..9939df7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,3 +2,5 @@ # Ignore build and test binaries. bin/ testbin/ + +.vscode diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ece4c1d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,147 @@ +# 定义工作流的名称 +name: CI + +on: + workflow_dispatch: # 允许手动触发工作流 + push: # 当有代码推送事件发生时 + branches: + - '*' # 只有推送到 main 分支时才触发 + tags: + - '*' + pull_request: + + +# 定义工作流的任务 +jobs: + build: + name: Set up Build + runs-on: ubuntu-latest + + # 定义任务中的步骤 + steps: + # 检出代码。只检出最新的1次提交 + - uses: actions/checkout@v3 + with: + fetch-depth: 1 + + # 设置 QEMU 模拟器,这通常用于多平台的 Docker 构建 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + # 设置 Docker Buildx,用于构建多平台的 Docker 镜像 + - name: Set up Build + uses: docker/setup-buildx-action@v2 + - run: | + ls -l + + linting: + name: Lint Code Base + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + # Full git history is needed to get a proper list of changed files within `super-linter` + fetch-depth: 0 + - name: Lint Code Base + uses: github/super-linter@v4 + env: + VALIDATE_MARKDOWN: true + VALIDATE_ALL_CODEBASE: false + DEFAULT_BRANCH: main + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + LINTER_RULES_PATH: / + MARKDOWN_CONFIG_FILE: .markdownlint.yml + + go-test: + name: Go Test + runs-on: ubuntu-latest + + # Service containers to run with `runner-job` + services: + # Label used to access the service container + postgres: + # Docker Hub image + image: postgres + # Provide the password for postgres + env: + POSTGRES_PASSWORD: postgres + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + # Maps tcp port 5432 on service container to the host + - 5432:5432 + + redis: + # Docker Hub image + image: redis + # Set health checks to wait until redis has started + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + # Maps port 6379 on service container to the host + - 6379:6379 + + mysql: + image: mysql:5.7 + env: + MYSQL_DATABASE: test_db + MYSQL_USER: mysql + MYSQL_PASSWORD: mysql + MYSQL_ROOT_PASSWORD: mysql + ports: + - 33306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + minio: + # fixme: let's not depend on external unofficial image + image: bitnami/minio:latest + ports: + - 9000:9000 + - 9090:9090 + env: + MINIO_ROOT_USER: admin + MINIO_ROOT_PASSWORD: admin123456 + options: --name=minio --health-cmd "curl http://localhost:9000/minio/health/live" + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + # Full git history is needed to get a proper list of changed files within `super-linter` + fetch-depth: 0 + - name: Go Test + run: | + docker ps -a + go test -v ./... + env: + # The hostname used to communicate with the PostgreSQL service container + PG_DB_HOST: "localhost" + PG_DB_PORT: "5432" + PG_DB_SSLMODE: "false" + PG_DB_USERNAME: "postgres" + PG_DB_PASSWORD: "postgres" + + # The hostname used to communicate with the Redis service container + REDIS_ADDR: "localhost:6379" + + # The hostname used to communicate with the Mysql service container + MYSQL_DB_HOST: "localhost" + MYSQL_DB_PORT: "33306" + MYSQL_DB_SSLMODE: "false" + MYSQL_DB_USERNAME: "root" + MYSQL_DB_PASSWORD: "mysql" + + # The hostname used to communicate with the Minio service container + MINIO_ENDPOINT: "localhost:9000" + MINIO_ACCESS_KEY: "admin" + MINIO_SECRET_KEY: "admin123456" + MINIO_SSL: "false" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3af899b..1fdd55d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,5 @@ Dockerfile.cross *.swo *~ -.env \ No newline at end of file +.env +.vscode \ No newline at end of file diff --git a/.markdownlint.yml b/.markdownlint.yml new file mode 100644 index 0000000..45d588e --- /dev/null +++ b/.markdownlint.yml @@ -0,0 +1,20 @@ +MD007: + indent: 4 + start_indented: false + +MD013: + line_length: 200 + heading_line_length: 80 + code_block_line_length: 200 + code_blocks: true + tables: true + headings: true + headers: true + strict: false + stern: false + +MD012: + maximum: 20 + +MD046: false +MD051: false \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index f980ab9..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - // 使用 IntelliSense 了解相关属性。 - // 悬停以查看现有属性的描述。 - // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [] -} \ No newline at end of file diff --git a/PROJECT b/PROJECT index d1a6fc4..fd71268 100644 --- a/PROJECT +++ b/PROJECT @@ -29,4 +29,31 @@ resources: kind: Database path: github.com/zncdata-labs/zncdata-stack-operator/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: zncdata.net + group: stack + kind: S3Connection + path: github.com/zncdata-labs/zncdata-stack-operator/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: zncdata.net + group: stack + kind: S3Bucket + path: github.com/zncdata-labs/zncdata-stack-operator/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: zncdata.net + group: stack + kind: RedisConnection + path: github.com/zncdata-labs/zncdata-stack-operator/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/v1alpha1/conditions.go b/api/v1alpha1/conditions.go new file mode 100644 index 0000000..485dc92 --- /dev/null +++ b/api/v1alpha1/conditions.go @@ -0,0 +1,15 @@ +package v1alpha1 + +const ( + ConditionTypeProgressing string = "Progressing" + ConditionTypeReconcile string = "Reconcile" + ConditionTypeAvailable string = "Available" + + ConditionReasonPreparing string = "Preparing" + ConditionReasonRunning string = "Running" + ConditionReasonConfig string = "Config" + ConditionReasonReconcilePVC string = "ReconcilePVC" + ConditionReasonReconcileService string = "ReconcileService" + ConditionReasonReconcileIngress string = "ReconcileIngress" + ConditionReasonReconcileDeployment string = "ReconcileDeployment" +) diff --git a/api/v1alpha1/database_types.go b/api/v1alpha1/database_types.go index a8e6f93..990b8ce 100644 --- a/api/v1alpha1/database_types.go +++ b/api/v1alpha1/database_types.go @@ -17,22 +17,23 @@ limitations under the License. package v1alpha1 import ( + apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +const DatabaseFinalizer = "database.finalizers.stack.zncdata.net" + // DatabaseSpec defines the desired state of Database type DatabaseSpec struct { - Name string `json:"name,omitempty"` - Connection *ConnectionSpec `json:"connection,omitempty"` - Credential *CredentialSpec `json:"credential,omitempty"` + Name string `json:"name,omitempty"` + Reference string `json:"reference,omitempty"` + Credential *DatabaseCredentialSpec `json:"credential,omitempty"` } -type ConnectionSpec struct { - Reference string `json:"reference,omitempty"` - Driver string `json:"driver,omitempty"` - Host string `json:"host,omitempty"` - Port int `json:"port,omitempty"` - SSL bool `json:"ssl,omitempty"` +type DatabaseCredentialSpec struct { + ExistSecret string `json:"existingSecret,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` } // DatabaseStatus defines the observed state of Database @@ -53,6 +54,33 @@ type Database struct { Status DatabaseStatus `json:"status,omitempty"` } +func (in *Database) GetNameWithSuffix(s string) string { + return in.Name + "-" + s +} + +func (in *Database) SetStatusCondition(condition metav1.Condition) { + // if the condition already exists, update it + existingCondition := apimeta.FindStatusCondition(in.Status.Conditions, condition.Type) + if existingCondition == nil { + condition.ObservedGeneration = in.GetGeneration() + condition.LastTransitionTime = metav1.Now() + in.Status.Conditions = append(in.Status.Conditions, condition) + } else if existingCondition.Status != condition.Status || existingCondition.Reason != condition.Reason || existingCondition.Message != condition.Message { + existingCondition.Status = condition.Status + existingCondition.Reason = condition.Reason + existingCondition.Message = condition.Message + existingCondition.ObservedGeneration = in.GetGeneration() + existingCondition.LastTransitionTime = metav1.Now() + } +} + +func (in *Database) IsAvailable() bool { + if cond := apimeta.FindStatusCondition(in.Status.Conditions, ConditionTypeAvailable); cond != nil && cond.Status == metav1.ConditionTrue && string(cond.Status) == ConditionReasonRunning { + return true + } + return false +} + //+kubebuilder:object:root=true // DatabaseList contains a list of Database diff --git a/api/v1alpha1/databaseconnection_types.go b/api/v1alpha1/databaseconnection_types.go index 34c3390..d53a034 100644 --- a/api/v1alpha1/databaseconnection_types.go +++ b/api/v1alpha1/databaseconnection_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1alpha1 import ( + apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -26,22 +27,21 @@ import ( // DatabaseConnectionSpec defines the desired state of DatabaseConnection type DatabaseConnectionSpec struct { Provider *ProviderSpec `json:"provider,omitempty"` + Default bool `json:"default,omitempty"` } type ProviderSpec struct { // +kubebuilder:validation:Enum=mysql;postgres // +kubebulider:default=postgres - Driver string `json:"driver,omitempty"` - Host string `json:"host,omitempty"` - Port int `json:"port,omitempty"` - SSL bool `json:"ssl,omitempty"` - Credential *CredentialSpec `json:"credential,omitempty"` + Driver string `json:"driver,omitempty"` + Host string `json:"host,omitempty"` + Port int `json:"port,omitempty"` + SSL bool `json:"ssl,omitempty"` + Credential *DatabaseConnectionCredentialSpec `json:"credential,omitempty"` } -type CredentialSpec struct { - ExistSecret string `json:"existSecret,omitempty"` - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` +type DatabaseConnectionCredentialSpec struct { + ExistSecret string `json:"existingSecret,omitempty"` } // DatabaseConnectionStatus defines the observed state of DatabaseConnection @@ -74,3 +74,50 @@ type DatabaseConnectionList struct { func init() { SchemeBuilder.Register(&DatabaseConnection{}, &DatabaseConnectionList{}) } + +// SetStatusCondition updates the status condition using the provided arguments. +// If the condition already exists, it updates the condition; otherwise, it appends the condition. +// If the condition status has changed, it updates the condition's LastTransitionTime. +func (r *DatabaseConnection) SetStatusCondition(condition metav1.Condition) { + // if the condition already exists, update it + existingCondition := apimeta.FindStatusCondition(r.Status.Conditions, condition.Type) + if existingCondition == nil { + condition.ObservedGeneration = r.GetGeneration() + condition.LastTransitionTime = metav1.Now() + r.Status.Conditions = append(r.Status.Conditions, condition) + } else if existingCondition.Status != condition.Status || existingCondition.Reason != condition.Reason || existingCondition.Message != condition.Message { + existingCondition.Status = condition.Status + existingCondition.Reason = condition.Reason + existingCondition.Message = condition.Message + existingCondition.ObservedGeneration = r.GetGeneration() + existingCondition.LastTransitionTime = metav1.Now() + } +} + +// InitStatusConditions initializes the status conditions to the provided conditions. +func (r *DatabaseConnection) InitStatusConditions() { + r.Status.Conditions = []metav1.Condition{} + r.SetStatusCondition(metav1.Condition{ + Type: ConditionTypeProgressing, + Status: metav1.ConditionTrue, + Reason: ConditionReasonPreparing, + Message: "DatabaseConnection is preparing", + ObservedGeneration: r.GetGeneration(), + LastTransitionTime: metav1.Now(), + }) + r.SetStatusCondition(metav1.Condition{ + Type: ConditionTypeAvailable, + Status: metav1.ConditionFalse, + Reason: ConditionReasonPreparing, + Message: "DatabaseConnection is preparing", + ObservedGeneration: r.GetGeneration(), + LastTransitionTime: metav1.Now(), + }) +} + +func (r *DatabaseConnection) IsAvailable() bool { + if cond := apimeta.FindStatusCondition(r.Status.Conditions, ConditionTypeAvailable); cond != nil && cond.Status == metav1.ConditionTrue && string(cond.Status) == ConditionReasonRunning { + return true + } + return false +} diff --git a/api/v1alpha1/redisconnection_types.go b/api/v1alpha1/redisconnection_types.go new file mode 100644 index 0000000..020c847 --- /dev/null +++ b/api/v1alpha1/redisconnection_types.go @@ -0,0 +1,109 @@ +/* +Copyright 2023 zncdata-labs. + +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 v1alpha1 + +import ( + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// RedisConnectionSpec defines the desired state of RedisConnection +type RedisConnectionSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + Host string `json:"host,omitempty"` + Port string `json:"port,omitempty"` + Password string `json:"password,omitempty"` +} + +// RedisConnectionStatus defines the observed state of RedisConnection +type RedisConnectionStatus struct { + // +kubebuilder:validation:Optional + Conditions []metav1.Condition `json:"condition,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// RedisConnection is the Schema for the redisconnections API +type RedisConnection struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RedisConnectionSpec `json:"spec,omitempty"` + Status RedisConnectionStatus `json:"status,omitempty"` +} + +func (r *RedisConnection) SetStatusCondition(condition metav1.Condition) { + // if the condition already exists, update it + existingCondition := apimeta.FindStatusCondition(r.Status.Conditions, condition.Type) + if existingCondition == nil { + condition.ObservedGeneration = r.GetGeneration() + condition.LastTransitionTime = metav1.Now() + r.Status.Conditions = append(r.Status.Conditions, condition) + } else if existingCondition.Status != condition.Status || existingCondition.Reason != condition.Reason || existingCondition.Message != condition.Message { + existingCondition.Status = condition.Status + existingCondition.Reason = condition.Reason + existingCondition.Message = condition.Message + existingCondition.ObservedGeneration = r.GetGeneration() + existingCondition.LastTransitionTime = metav1.Now() + } +} + +func (r *RedisConnection) IsAvailable() bool { + if cond := apimeta.FindStatusCondition(r.Status.Conditions, ConditionTypeAvailable); cond != nil && cond.Status == metav1.ConditionTrue && string(cond.Status) == ConditionReasonRunning { + return true + } + return false +} + +func (r *RedisConnection) InitStatusConditions() { + r.Status.Conditions = []metav1.Condition{} + r.SetStatusCondition(metav1.Condition{ + Type: ConditionTypeProgressing, + Status: metav1.ConditionTrue, + Reason: ConditionReasonPreparing, + Message: "redisConnection is preparing", + ObservedGeneration: r.GetGeneration(), + LastTransitionTime: metav1.Now(), + }) + r.SetStatusCondition(metav1.Condition{ + Type: ConditionTypeAvailable, + Status: metav1.ConditionFalse, + Reason: ConditionReasonPreparing, + Message: "redisConnection is preparing", + ObservedGeneration: r.GetGeneration(), + LastTransitionTime: metav1.Now(), + }) +} + +//+kubebuilder:object:root=true + +// RedisConnectionList contains a list of RedisConnection +type RedisConnectionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []RedisConnection `json:"items"` +} + +func init() { + SchemeBuilder.Register(&RedisConnection{}, &RedisConnectionList{}) +} diff --git a/api/v1alpha1/s3bucket_types.go b/api/v1alpha1/s3bucket_types.go new file mode 100644 index 0000000..c8b8b68 --- /dev/null +++ b/api/v1alpha1/s3bucket_types.go @@ -0,0 +1,119 @@ +/* +Copyright 2023 zncdata-labs. + +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 v1alpha1 + +import ( + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const S3BucketFinalizer = "s3bucket.finalizers.stack.zncdata.net" + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// S3BucketSpec defines the desired state of S3Bucket +type S3BucketSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + // +kubebuilder:validation:Required + Reference string `json:"reference,omitempty"` + // +kubebuilder:validation:Optional + Credential *S3BucketCredential `json:"credential,omitempty"` + Name string `json:"name,omitempty"` +} + +type S3BucketCredential struct { + // +kubebuilder:validation:Optional + ExistingSecret string `json:"existingSecret,omitempty"` +} + +// S3BucketStatus defines the observed state of S3Bucket +type S3BucketStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + + Conditions []metav1.Condition `json:"condition,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// S3Bucket is the Schema for the s3buckets API +type S3Bucket struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec S3BucketSpec `json:"spec,omitempty"` + Status S3BucketStatus `json:"status,omitempty"` +} + +func (s *S3Bucket) InitStatusConditions() { + s.Status.Conditions = []metav1.Condition{} + s.SetStatusCondition(metav1.Condition{ + Type: ConditionTypeProgressing, + Status: metav1.ConditionTrue, + Reason: ConditionReasonPreparing, + Message: "s3Connection is preparing", + ObservedGeneration: s.GetGeneration(), + LastTransitionTime: metav1.Now(), + }) + s.SetStatusCondition(metav1.Condition{ + Type: ConditionTypeAvailable, + Status: metav1.ConditionFalse, + Reason: ConditionReasonPreparing, + Message: "s3Connection is preparing", + ObservedGeneration: s.GetGeneration(), + LastTransitionTime: metav1.Now(), + }) +} + +func (s *S3Bucket) SetStatusCondition(condition metav1.Condition) { + // if the condition already exists, update it + existingCondition := apimeta.FindStatusCondition(s.Status.Conditions, condition.Type) + if existingCondition == nil { + condition.ObservedGeneration = s.GetGeneration() + condition.LastTransitionTime = metav1.Now() + s.Status.Conditions = append(s.Status.Conditions, condition) + } else if existingCondition.Status != condition.Status || existingCondition.Reason != condition.Reason || existingCondition.Message != condition.Message { + existingCondition.Status = condition.Status + existingCondition.Reason = condition.Reason + existingCondition.Message = condition.Message + existingCondition.ObservedGeneration = s.GetGeneration() + existingCondition.LastTransitionTime = metav1.Now() + } +} + +func (s *S3Bucket) IsAvailable() bool { + if cond := apimeta.FindStatusCondition(s.Status.Conditions, ConditionTypeAvailable); cond != nil && cond.Status == metav1.ConditionTrue && string(cond.Status) == ConditionReasonRunning { + return true + } + return false +} + +//+kubebuilder:object:root=true + +// S3BucketList contains a list of S3Bucket +type S3BucketList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []S3Bucket `json:"items"` +} + +func init() { + SchemeBuilder.Register(&S3Bucket{}, &S3BucketList{}) +} diff --git a/api/v1alpha1/s3connection_types.go b/api/v1alpha1/s3connection_types.go new file mode 100644 index 0000000..8ccb4bf --- /dev/null +++ b/api/v1alpha1/s3connection_types.go @@ -0,0 +1,117 @@ +/* +Copyright 2023 zncdata-labs. + +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 v1alpha1 + +import ( + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// S3ConnectionSpec defines the desired state of S3Connection +type S3ConnectionSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + // +kubebuilder:validation:Required + S3Credential *S3Credential `json:"credential,omitempty"` +} + +type S3Credential struct { + // +kubebuilder:validation:Optional + ExistingSecret string `json:"existingSecret,omitempty"` + AccessKey string `json:"accessKey,omitempty"` + SecretKey string `json:"secretKey,omitempty"` + Endpoint string `json:"endpoint,omitempty"` + Region string `json:"region,omitempty"` + SSL bool `json:"ssl,omitempty"` +} + +// S3ConnectionStatus defines the observed state of S3Connection +type S3ConnectionStatus struct { + // +kubebuilder:validation:Optional + Conditions []metav1.Condition `json:"condition,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// S3Connection is the Schema for the s3connections API +type S3Connection struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec S3ConnectionSpec `json:"spec,omitempty"` + Status S3ConnectionStatus `json:"status,omitempty"` +} + +func (s *S3Connection) SetStatusCondition(condition metav1.Condition) { + // if the condition already exists, update it + existingCondition := apimeta.FindStatusCondition(s.Status.Conditions, condition.Type) + if existingCondition == nil { + condition.ObservedGeneration = s.GetGeneration() + condition.LastTransitionTime = metav1.Now() + s.Status.Conditions = append(s.Status.Conditions, condition) + } else if existingCondition.Status != condition.Status || existingCondition.Reason != condition.Reason || existingCondition.Message != condition.Message { + existingCondition.Status = condition.Status + existingCondition.Reason = condition.Reason + existingCondition.Message = condition.Message + existingCondition.ObservedGeneration = s.GetGeneration() + existingCondition.LastTransitionTime = metav1.Now() + } +} + +func (s *S3Connection) IsAvailable() bool { + if cond := apimeta.FindStatusCondition(s.Status.Conditions, ConditionTypeAvailable); cond != nil && cond.Status == metav1.ConditionTrue && string(cond.Status) == ConditionReasonRunning { + return true + } + return false +} + +func (s *S3Connection) InitStatusConditions() { + s.Status.Conditions = []metav1.Condition{} + s.SetStatusCondition(metav1.Condition{ + Type: ConditionTypeProgressing, + Status: metav1.ConditionTrue, + Reason: ConditionReasonPreparing, + Message: "s3Connection is preparing", + ObservedGeneration: s.GetGeneration(), + LastTransitionTime: metav1.Now(), + }) + s.SetStatusCondition(metav1.Condition{ + Type: ConditionTypeAvailable, + Status: metav1.ConditionFalse, + Reason: ConditionReasonPreparing, + Message: "s3Connection is preparing", + ObservedGeneration: s.GetGeneration(), + LastTransitionTime: metav1.Now(), + }) +} + +//+kubebuilder:object:root=true + +// S3ConnectionList contains a list of S3Connection +type S3ConnectionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []S3Connection `json:"items"` +} + +func init() { + SchemeBuilder.Register(&S3Connection{}, &S3ConnectionList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index c40da03..6a5511a 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -25,36 +25,6 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ConnectionSpec) DeepCopyInto(out *ConnectionSpec) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionSpec. -func (in *ConnectionSpec) DeepCopy() *ConnectionSpec { - if in == nil { - return nil - } - out := new(ConnectionSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *CredentialSpec) DeepCopyInto(out *CredentialSpec) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialSpec. -func (in *CredentialSpec) DeepCopy() *CredentialSpec { - if in == nil { - return nil - } - out := new(CredentialSpec) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Database) DeepCopyInto(out *Database) { *out = *in @@ -109,6 +79,21 @@ func (in *DatabaseConnection) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DatabaseConnectionCredentialSpec) DeepCopyInto(out *DatabaseConnectionCredentialSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseConnectionCredentialSpec. +func (in *DatabaseConnectionCredentialSpec) DeepCopy() *DatabaseConnectionCredentialSpec { + if in == nil { + return nil + } + out := new(DatabaseConnectionCredentialSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DatabaseConnectionList) DeepCopyInto(out *DatabaseConnectionList) { *out = *in @@ -183,6 +168,21 @@ func (in *DatabaseConnectionStatus) DeepCopy() *DatabaseConnectionStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DatabaseCredentialSpec) DeepCopyInto(out *DatabaseCredentialSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseCredentialSpec. +func (in *DatabaseCredentialSpec) DeepCopy() *DatabaseCredentialSpec { + if in == nil { + return nil + } + out := new(DatabaseCredentialSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DatabaseList) DeepCopyInto(out *DatabaseList) { *out = *in @@ -218,14 +218,9 @@ func (in *DatabaseList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DatabaseSpec) DeepCopyInto(out *DatabaseSpec) { *out = *in - if in.Connection != nil { - in, out := &in.Connection, &out.Connection - *out = new(ConnectionSpec) - **out = **in - } if in.Credential != nil { in, out := &in.Credential, &out.Credential - *out = new(CredentialSpec) + *out = new(DatabaseCredentialSpec) **out = **in } } @@ -267,7 +262,7 @@ func (in *ProviderSpec) DeepCopyInto(out *ProviderSpec) { *out = *in if in.Credential != nil { in, out := &in.Credential, &out.Credential - *out = new(CredentialSpec) + *out = new(DatabaseConnectionCredentialSpec) **out = **in } } @@ -281,3 +276,331 @@ func (in *ProviderSpec) DeepCopy() *ProviderSpec { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisConnection) DeepCopyInto(out *RedisConnection) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisConnection. +func (in *RedisConnection) DeepCopy() *RedisConnection { + if in == nil { + return nil + } + out := new(RedisConnection) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RedisConnection) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisConnectionList) DeepCopyInto(out *RedisConnectionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]RedisConnection, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisConnectionList. +func (in *RedisConnectionList) DeepCopy() *RedisConnectionList { + if in == nil { + return nil + } + out := new(RedisConnectionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RedisConnectionList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisConnectionSpec) DeepCopyInto(out *RedisConnectionSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisConnectionSpec. +func (in *RedisConnectionSpec) DeepCopy() *RedisConnectionSpec { + if in == nil { + return nil + } + out := new(RedisConnectionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisConnectionStatus) DeepCopyInto(out *RedisConnectionStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisConnectionStatus. +func (in *RedisConnectionStatus) DeepCopy() *RedisConnectionStatus { + if in == nil { + return nil + } + out := new(RedisConnectionStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *S3Bucket) DeepCopyInto(out *S3Bucket) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3Bucket. +func (in *S3Bucket) DeepCopy() *S3Bucket { + if in == nil { + return nil + } + out := new(S3Bucket) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *S3Bucket) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *S3BucketCredential) DeepCopyInto(out *S3BucketCredential) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3BucketCredential. +func (in *S3BucketCredential) DeepCopy() *S3BucketCredential { + if in == nil { + return nil + } + out := new(S3BucketCredential) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *S3BucketList) DeepCopyInto(out *S3BucketList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]S3Bucket, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3BucketList. +func (in *S3BucketList) DeepCopy() *S3BucketList { + if in == nil { + return nil + } + out := new(S3BucketList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *S3BucketList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *S3BucketSpec) DeepCopyInto(out *S3BucketSpec) { + *out = *in + if in.Credential != nil { + in, out := &in.Credential, &out.Credential + *out = new(S3BucketCredential) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3BucketSpec. +func (in *S3BucketSpec) DeepCopy() *S3BucketSpec { + if in == nil { + return nil + } + out := new(S3BucketSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *S3BucketStatus) DeepCopyInto(out *S3BucketStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3BucketStatus. +func (in *S3BucketStatus) DeepCopy() *S3BucketStatus { + if in == nil { + return nil + } + out := new(S3BucketStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *S3Connection) DeepCopyInto(out *S3Connection) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3Connection. +func (in *S3Connection) DeepCopy() *S3Connection { + if in == nil { + return nil + } + out := new(S3Connection) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *S3Connection) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *S3ConnectionList) DeepCopyInto(out *S3ConnectionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]S3Connection, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3ConnectionList. +func (in *S3ConnectionList) DeepCopy() *S3ConnectionList { + if in == nil { + return nil + } + out := new(S3ConnectionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *S3ConnectionList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *S3ConnectionSpec) DeepCopyInto(out *S3ConnectionSpec) { + *out = *in + if in.S3Credential != nil { + in, out := &in.S3Credential, &out.S3Credential + *out = new(S3Credential) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3ConnectionSpec. +func (in *S3ConnectionSpec) DeepCopy() *S3ConnectionSpec { + if in == nil { + return nil + } + out := new(S3ConnectionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *S3ConnectionStatus) DeepCopyInto(out *S3ConnectionStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3ConnectionStatus. +func (in *S3ConnectionStatus) DeepCopy() *S3ConnectionStatus { + if in == nil { + return nil + } + out := new(S3ConnectionStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *S3Credential) DeepCopyInto(out *S3Credential) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3Credential. +func (in *S3Credential) DeepCopy() *S3Credential { + if in == nil { + return nil + } + out := new(S3Credential) + in.DeepCopyInto(out) + return out +} diff --git a/bundle.Dockerfile b/bundle.Dockerfile new file mode 100644 index 0000000..b3c1360 --- /dev/null +++ b/bundle.Dockerfile @@ -0,0 +1,20 @@ +FROM scratch + +# Core bundle labels. +LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 +LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ +LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ +LABEL operators.operatorframework.io.bundle.package.v1=zncdata-stack-operator +LABEL operators.operatorframework.io.bundle.channels.v1=alpha +LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.32.0 +LABEL operators.operatorframework.io.metrics.mediatype.v1=metrics+v1 +LABEL operators.operatorframework.io.metrics.project_layout=go.kubebuilder.io/v4-alpha + +# Labels for testing. +LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1 +LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/ + +# Copy files to locations specified by labels. +COPY bundle/manifests /manifests/ +COPY bundle/metadata /metadata/ +COPY bundle/tests/scorecard /tests/scorecard/ diff --git a/bundle/manifests/stack.zncdata.net_databaseconnections.yaml b/bundle/manifests/stack.zncdata.net_databaseconnections.yaml new file mode 100644 index 0000000..0712ddc --- /dev/null +++ b/bundle/manifests/stack.zncdata.net_databaseconnections.yaml @@ -0,0 +1,146 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + creationTimestamp: null + name: databaseconnections.stack.zncdata.net +spec: + group: stack.zncdata.net + names: + kind: DatabaseConnection + listKind: DatabaseConnectionList + plural: databaseconnections + singular: databaseconnection + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: DatabaseConnection is the Schema for the databaseconnections + API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: DatabaseConnectionSpec defines the desired state of DatabaseConnection + properties: + default: + type: boolean + provider: + properties: + credential: + properties: + exist_secret: + type: string + password: + type: string + username: + type: string + type: object + driver: + enum: + - mysql + - postgres + type: string + host: + type: string + port: + type: integer + ssl: + type: boolean + type: object + type: object + status: + description: DatabaseConnectionStatus defines the observed state of DatabaseConnection + properties: + condition: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/bundle/manifests/stack.zncdata.net_databases.yaml b/bundle/manifests/stack.zncdata.net_databases.yaml new file mode 100644 index 0000000..10846c6 --- /dev/null +++ b/bundle/manifests/stack.zncdata.net_databases.yaml @@ -0,0 +1,133 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + creationTimestamp: null + name: databases.stack.zncdata.net +spec: + group: stack.zncdata.net + names: + kind: Database + listKind: DatabaseList + plural: databases + singular: database + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Database is the Schema for the databases API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: DatabaseSpec defines the desired state of Database + properties: + credential: + properties: + exist_secret: + type: string + password: + type: string + username: + type: string + type: object + name: + type: string + reference: + type: string + type: object + status: + description: DatabaseStatus defines the observed state of Database + properties: + condition: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/bundle/manifests/stack.zncdata.net_redisconnections.yaml b/bundle/manifests/stack.zncdata.net_redisconnections.yaml new file mode 100644 index 0000000..fa26ac9 --- /dev/null +++ b/bundle/manifests/stack.zncdata.net_redisconnections.yaml @@ -0,0 +1,124 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + creationTimestamp: null + name: redisconnections.stack.zncdata.net +spec: + group: stack.zncdata.net + names: + kind: RedisConnection + listKind: RedisConnectionList + plural: redisconnections + singular: redisconnection + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: RedisConnection is the Schema for the redisconnections API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: RedisConnectionSpec defines the desired state of RedisConnection + properties: + host: + type: string + port: + type: string + type: object + status: + description: RedisConnectionStatus defines the observed state of RedisConnection + properties: + condition: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/bundle/manifests/stack.zncdata.net_s3buckets.yaml b/bundle/manifests/stack.zncdata.net_s3buckets.yaml new file mode 100644 index 0000000..71a86de --- /dev/null +++ b/bundle/manifests/stack.zncdata.net_s3buckets.yaml @@ -0,0 +1,55 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + creationTimestamp: null + name: s3buckets.stack.zncdata.net +spec: + group: stack.zncdata.net + names: + kind: S3Bucket + listKind: S3BucketList + plural: s3buckets + singular: s3bucket + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: S3Bucket is the Schema for the s3buckets API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: S3BucketSpec defines the desired state of S3Bucket + properties: + foo: + description: Foo is an example field of S3Bucket. Edit s3bucket_types.go + to remove/update + type: string + type: object + status: + description: S3BucketStatus defines the observed state of S3Bucket + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/bundle/manifests/stack.zncdata.net_s3connections.yaml b/bundle/manifests/stack.zncdata.net_s3connections.yaml new file mode 100644 index 0000000..b2701bd --- /dev/null +++ b/bundle/manifests/stack.zncdata.net_s3connections.yaml @@ -0,0 +1,55 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + creationTimestamp: null + name: s3connections.stack.zncdata.net +spec: + group: stack.zncdata.net + names: + kind: S3Connection + listKind: S3ConnectionList + plural: s3connections + singular: s3connection + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: S3Connection is the Schema for the s3connections API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: S3ConnectionSpec defines the desired state of S3Connection + properties: + foo: + description: Foo is an example field of S3Connection. Edit s3connection_types.go + to remove/update + type: string + type: object + status: + description: S3ConnectionStatus defines the observed state of S3Connection + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/bundle/manifests/zncdata-stack-operator-controller-manager-metrics-service_v1_service.yaml b/bundle/manifests/zncdata-stack-operator-controller-manager-metrics-service_v1_service.yaml new file mode 100644 index 0000000..ab9249b --- /dev/null +++ b/bundle/manifests/zncdata-stack-operator-controller-manager-metrics-service_v1_service.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + creationTimestamp: null + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: zncdata-stack-operator + app.kubernetes.io/instance: controller-manager-metrics-service + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: service + app.kubernetes.io/part-of: zncdata-stack-operator + control-plane: controller-manager + name: zncdata-stack-operator-controller-manager-metrics-service +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager +status: + loadBalancer: {} diff --git a/bundle/manifests/zncdata-stack-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml b/bundle/manifests/zncdata-stack-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml new file mode 100644 index 0000000..77dd55d --- /dev/null +++ b/bundle/manifests/zncdata-stack-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml @@ -0,0 +1,17 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: zncdata-stack-operator + app.kubernetes.io/instance: metrics-reader + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: clusterrole + app.kubernetes.io/part-of: zncdata-stack-operator + name: zncdata-stack-operator-metrics-reader +rules: +- nonResourceURLs: + - /metrics + verbs: + - get diff --git a/bundle/manifests/zncdata-stack-operator.clusterserviceversion.yaml b/bundle/manifests/zncdata-stack-operator.clusterserviceversion.yaml new file mode 100644 index 0000000..237f703 --- /dev/null +++ b/bundle/manifests/zncdata-stack-operator.clusterserviceversion.yaml @@ -0,0 +1,437 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: |- + [ + { + "apiVersion": "stack.zncdata.net/v1alpha1", + "kind": "Database", + "metadata": { + "labels": { + "app.kubernetes.io/created-by": "zncdata-stack-operator", + "app.kubernetes.io/instance": "database-sample", + "app.kubernetes.io/managed-by": "kustomize", + "app.kubernetes.io/name": "database", + "app.kubernetes.io/part-of": "zncdata-stack-operator" + }, + "name": "database-sample" + }, + "spec": null + }, + { + "apiVersion": "stack.zncdata.net/v1alpha1", + "kind": "DatabaseConnection", + "metadata": { + "labels": { + "app.kubernetes.io/created-by": "zncdata-stack-operator", + "app.kubernetes.io/instance": "databaseconnection-sample", + "app.kubernetes.io/managed-by": "kustomize", + "app.kubernetes.io/name": "databaseconnection", + "app.kubernetes.io/part-of": "zncdata-stack-operator" + }, + "name": "databaseconnection-sample" + }, + "spec": null + }, + { + "apiVersion": "stack.zncdata.net/v1alpha1", + "kind": "RedisConnection", + "metadata": { + "labels": { + "app.kubernetes.io/created-by": "zncdata-stack-operator", + "app.kubernetes.io/instance": "redisconnection-sample", + "app.kubernetes.io/managed-by": "kustomize", + "app.kubernetes.io/name": "redisconnection", + "app.kubernetes.io/part-of": "zncdata-stack-operator" + }, + "name": "redisconnection-sample" + }, + "spec": null + }, + { + "apiVersion": "stack.zncdata.net/v1alpha1", + "kind": "S3Bucket", + "metadata": { + "labels": { + "app.kubernetes.io/created-by": "zncdata-stack-operator", + "app.kubernetes.io/instance": "s3bucket-sample", + "app.kubernetes.io/managed-by": "kustomize", + "app.kubernetes.io/name": "s3bucket", + "app.kubernetes.io/part-of": "zncdata-stack-operator" + }, + "name": "s3bucket-sample" + }, + "spec": null + }, + { + "apiVersion": "stack.zncdata.net/v1alpha1", + "kind": "S3Connection", + "metadata": { + "labels": { + "app.kubernetes.io/created-by": "zncdata-stack-operator", + "app.kubernetes.io/instance": "s3connection-sample", + "app.kubernetes.io/managed-by": "kustomize", + "app.kubernetes.io/name": "s3connection", + "app.kubernetes.io/part-of": "zncdata-stack-operator" + }, + "name": "s3connection-sample" + }, + "spec": null + } + ] + capabilities: Basic Install + createdAt: "2023-12-07T03:52:56Z" + operators.operatorframework.io/builder: operator-sdk-v1.32.0 + operators.operatorframework.io/project_layout: go.kubebuilder.io/v4-alpha + name: zncdata-stack-operator.v0.0.1 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: DatabaseConnection is the Schema for the databaseconnections API + displayName: Database Connection + kind: DatabaseConnection + name: databaseconnections.stack.zncdata.net + version: v1alpha1 + - description: Database is the Schema for the databases API + displayName: Database + kind: Database + name: databases.stack.zncdata.net + version: v1alpha1 + - description: RedisConnection is the Schema for the redisconnections API + displayName: Redis Connection + kind: RedisConnection + name: redisconnections.stack.zncdata.net + version: v1alpha1 + - description: S3Bucket is the Schema for the s3buckets API + displayName: S3 Bucket + kind: S3Bucket + name: s3buckets.stack.zncdata.net + version: v1alpha1 + - description: S3Connection is the Schema for the s3connections API + displayName: S3 Connection + kind: S3Connection + name: s3connections.stack.zncdata.net + version: v1alpha1 + description: provider some special resource to other operator + displayName: zncdata-stack-operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + clusterPermissions: + - rules: + - apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - stack.zncdata.net + resources: + - databaseconnections + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - stack.zncdata.net + resources: + - databaseconnections/finalizers + verbs: + - update + - apiGroups: + - stack.zncdata.net + resources: + - databaseconnections/status + verbs: + - get + - patch + - update + - apiGroups: + - stack.zncdata.net + resources: + - databases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - stack.zncdata.net + resources: + - databases/finalizers + verbs: + - update + - apiGroups: + - stack.zncdata.net + resources: + - databases/status + verbs: + - get + - patch + - update + - apiGroups: + - stack.zncdata.net + resources: + - redisconnections + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - stack.zncdata.net + resources: + - redisconnections/finalizers + verbs: + - update + - apiGroups: + - stack.zncdata.net + resources: + - redisconnections/status + verbs: + - get + - patch + - update + - apiGroups: + - stack.zncdata.net + resources: + - s3buckets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - stack.zncdata.net + resources: + - s3buckets/finalizers + verbs: + - update + - apiGroups: + - stack.zncdata.net + resources: + - s3buckets/status + verbs: + - get + - patch + - update + - apiGroups: + - stack.zncdata.net + resources: + - s3connections + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - stack.zncdata.net + resources: + - s3connections/finalizers + verbs: + - update + - apiGroups: + - stack.zncdata.net + resources: + - s3connections/status + verbs: + - get + - patch + - update + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + serviceAccountName: zncdata-stack-operator-controller-manager + deployments: + - label: + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: zncdata-stack-operator + app.kubernetes.io/instance: controller-manager + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: deployment + app.kubernetes.io/part-of: zncdata-stack-operator + control-plane: controller-manager + name: zncdata-stack-operator-controller-manager + spec: + replicas: 1 + selector: + matchLabels: + control-plane: controller-manager + strategy: {} + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + - arm64 + - ppc64le + - s390x + - key: kubernetes.io/os + operator: In + values: + - linux + containers: + - args: + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --logtostderr=true + - --v=0 + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.1 + name: kube-rbac-proxy + ports: + - containerPort: 8443 + name: https + protocol: TCP + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + - args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=127.0.0.1:8080 + - --leader-elect + command: + - /manager + image: controller:latest + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + securityContext: + runAsNonRoot: true + serviceAccountName: zncdata-stack-operator-controller-manager + terminationGracePeriodSeconds: 10 + permissions: + - rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + serviceAccountName: zncdata-stack-operator-controller-manager + strategy: deployment + installModes: + - supported: false + type: OwnNamespace + - supported: false + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces + keywords: + - base + - s3 + - database + - redis + - authentik + links: + - name: Zncdata Stack Operator + url: https://zncdata-stack-operator.domain + maturity: alpha + provider: + name: zncdata.net + version: 0.0.1 diff --git a/bundle/metadata/annotations.yaml b/bundle/metadata/annotations.yaml new file mode 100644 index 0000000..1e54ba6 --- /dev/null +++ b/bundle/metadata/annotations.yaml @@ -0,0 +1,14 @@ +annotations: + # Core bundle annotations. + operators.operatorframework.io.bundle.mediatype.v1: registry+v1 + operators.operatorframework.io.bundle.manifests.v1: manifests/ + operators.operatorframework.io.bundle.metadata.v1: metadata/ + operators.operatorframework.io.bundle.package.v1: zncdata-stack-operator + operators.operatorframework.io.bundle.channels.v1: alpha + operators.operatorframework.io.metrics.builder: operator-sdk-v1.32.0 + operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 + operators.operatorframework.io.metrics.project_layout: go.kubebuilder.io/v4-alpha + + # Annotations for testing. + operators.operatorframework.io.test.mediatype.v1: scorecard+v1 + operators.operatorframework.io.test.config.v1: tests/scorecard/ diff --git a/bundle/tests/scorecard/config.yaml b/bundle/tests/scorecard/config.yaml new file mode 100644 index 0000000..0c0690c --- /dev/null +++ b/bundle/tests/scorecard/config.yaml @@ -0,0 +1,70 @@ +apiVersion: scorecard.operatorframework.io/v1alpha3 +kind: Configuration +metadata: + name: config +stages: +- parallel: true + tests: + - entrypoint: + - scorecard-test + - basic-check-spec + image: quay.io/operator-framework/scorecard-test:v1.31.0 + labels: + suite: basic + test: basic-check-spec-test + storage: + spec: + mountPath: {} + - entrypoint: + - scorecard-test + - olm-bundle-validation + image: quay.io/operator-framework/scorecard-test:v1.31.0 + labels: + suite: olm + test: olm-bundle-validation-test + storage: + spec: + mountPath: {} + - entrypoint: + - scorecard-test + - olm-crds-have-validation + image: quay.io/operator-framework/scorecard-test:v1.31.0 + labels: + suite: olm + test: olm-crds-have-validation-test + storage: + spec: + mountPath: {} + - entrypoint: + - scorecard-test + - olm-crds-have-resources + image: quay.io/operator-framework/scorecard-test:v1.31.0 + labels: + suite: olm + test: olm-crds-have-resources-test + storage: + spec: + mountPath: {} + - entrypoint: + - scorecard-test + - olm-spec-descriptors + image: quay.io/operator-framework/scorecard-test:v1.31.0 + labels: + suite: olm + test: olm-spec-descriptors-test + storage: + spec: + mountPath: {} + - entrypoint: + - scorecard-test + - olm-status-descriptors + image: quay.io/operator-framework/scorecard-test:v1.31.0 + labels: + suite: olm + test: olm-status-descriptors-test + storage: + spec: + mountPath: {} +storage: + spec: + mountPath: {} diff --git a/cmd/main.go b/cmd/main.go index f0cc687..6a4e179 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -18,6 +18,8 @@ package main import ( "flag" + "github.com/zncdata-labs/zncdata-stack-operator/internal/controller/redis" + "github.com/zncdata-labs/zncdata-stack-operator/internal/controller/s3" "os" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) @@ -37,8 +39,13 @@ import ( ) var ( - scheme = runtime.NewScheme() - setupLog = ctrl.Log.WithName("setup") + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") + s3BucketReconcilerLog = ctrl.Log.WithName("s3bucket-reconcilier") + s3ConnectionReconcilerLog = ctrl.Log.WithName("s3-connection-reconcilier") + redisConnectionReconcilerLog = ctrl.Log.WithName("redis-connection-reconcilier") + databaseConnectionReconcilerLog = ctrl.Log.WithName("database-connection-reconcilier") + databaseReconcilerLog = ctrl.Log.WithName("database-reconcilier") ) func init() { @@ -90,6 +97,7 @@ func main() { if err = (&dbController.DatabaseConnectionReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), + Log: databaseConnectionReconcilerLog, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "DatabaseConnection") os.Exit(1) @@ -97,10 +105,35 @@ func main() { if err = (&dbController.DatabaseReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), + Log: databaseReconcilerLog, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Database") os.Exit(1) } + if err = (&s3.S3ConnectionReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Log: s3ConnectionReconcilerLog, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "S3Connection") + os.Exit(1) + } + if err = (&s3.S3BucketReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Log: s3BucketReconcilerLog, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "S3Bucket") + os.Exit(1) + } + if err = (&redis.RedisConnectionReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Log: redisConnectionReconcilerLog, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "RedisConnection") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/config/crd/bases/stack.zncdata.net_databaseconnections.yaml b/config/crd/bases/stack.zncdata.net_databaseconnections.yaml index 5a957c7..d9be0a8 100644 --- a/config/crd/bases/stack.zncdata.net_databaseconnections.yaml +++ b/config/crd/bases/stack.zncdata.net_databaseconnections.yaml @@ -35,15 +35,13 @@ spec: spec: description: DatabaseConnectionSpec defines the desired state of DatabaseConnection properties: + default: + type: boolean provider: properties: credential: properties: - existSecret: - type: string - password: - type: string - username: + existingSecret: type: string type: object driver: diff --git a/config/crd/bases/stack.zncdata.net_databases.yaml b/config/crd/bases/stack.zncdata.net_databases.yaml index b85a044..f447661 100644 --- a/config/crd/bases/stack.zncdata.net_databases.yaml +++ b/config/crd/bases/stack.zncdata.net_databases.yaml @@ -34,22 +34,9 @@ spec: spec: description: DatabaseSpec defines the desired state of Database properties: - connection: - properties: - driver: - type: string - host: - type: string - port: - type: integer - reference: - type: string - ssl: - type: boolean - type: object credential: properties: - existSecret: + existingSecret: type: string password: type: string @@ -58,6 +45,8 @@ spec: type: object name: type: string + reference: + type: string type: object status: description: DatabaseStatus defines the observed state of Database diff --git a/config/crd/bases/stack.zncdata.net_redisconnections.yaml b/config/crd/bases/stack.zncdata.net_redisconnections.yaml new file mode 100644 index 0000000..b9810ab --- /dev/null +++ b/config/crd/bases/stack.zncdata.net_redisconnections.yaml @@ -0,0 +1,120 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: redisconnections.stack.zncdata.net +spec: + group: stack.zncdata.net + names: + kind: RedisConnection + listKind: RedisConnectionList + plural: redisconnections + singular: redisconnection + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: RedisConnection is the Schema for the redisconnections API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: RedisConnectionSpec defines the desired state of RedisConnection + properties: + host: + type: string + password: + type: string + port: + type: string + type: object + status: + description: RedisConnectionStatus defines the observed state of RedisConnection + properties: + condition: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/stack.zncdata.net_s3buckets.yaml b/config/crd/bases/stack.zncdata.net_s3buckets.yaml new file mode 100644 index 0000000..f91f447 --- /dev/null +++ b/config/crd/bases/stack.zncdata.net_s3buckets.yaml @@ -0,0 +1,125 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: s3buckets.stack.zncdata.net +spec: + group: stack.zncdata.net + names: + kind: S3Bucket + listKind: S3BucketList + plural: s3buckets + singular: s3bucket + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: S3Bucket is the Schema for the s3buckets API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: S3BucketSpec defines the desired state of S3Bucket + properties: + credential: + properties: + existingSecret: + type: string + type: object + name: + type: string + reference: + description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + Important: Run "make" to regenerate code after modifying this file' + type: string + type: object + status: + description: S3BucketStatus defines the observed state of S3Bucket + properties: + condition: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/stack.zncdata.net_s3connections.yaml b/config/crd/bases/stack.zncdata.net_s3connections.yaml new file mode 100644 index 0000000..36d5120 --- /dev/null +++ b/config/crd/bases/stack.zncdata.net_s3connections.yaml @@ -0,0 +1,131 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: s3connections.stack.zncdata.net +spec: + group: stack.zncdata.net + names: + kind: S3Connection + listKind: S3ConnectionList + plural: s3connections + singular: s3connection + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: S3Connection is the Schema for the s3connections API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: S3ConnectionSpec defines the desired state of S3Connection + properties: + credential: + description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + Important: Run "make" to regenerate code after modifying this file' + properties: + accessKey: + type: string + endpoint: + type: string + existingSecret: + type: string + region: + type: string + secretKey: + type: string + ssl: + type: boolean + type: object + type: object + status: + description: S3ConnectionStatus defines the observed state of S3Connection + properties: + condition: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index a86c4da..22759a2 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -4,6 +4,9 @@ resources: - bases/stack.zncdata.net_databaseconnections.yaml - bases/stack.zncdata.net_databases.yaml +- bases/stack.zncdata.net_s3connections.yaml +- bases/stack.zncdata.net_s3buckets.yaml +- bases/stack.zncdata.net_redisconnections.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -11,12 +14,18 @@ patchesStrategicMerge: # patches here are for enabling the conversion webhook for each CRD #- patches/webhook_in_databaseconnections.yaml #- patches/webhook_in_databases.yaml +#- patches/webhook_in_s3connections.yaml +#- patches/webhook_in_s3buckets.yaml +#- patches/webhook_in_redisconnections.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD #- patches/cainjection_in_databaseconnections.yaml #- patches/cainjection_in_databases.yaml +#- patches/cainjection_in_s3connections.yaml +#- patches/cainjection_in_s3buckets.yaml +#- patches/cainjection_in_redisconnections.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_redisconnections.yaml b/config/crd/patches/cainjection_in_redisconnections.yaml new file mode 100644 index 0000000..f34be25 --- /dev/null +++ b/config/crd/patches/cainjection_in_redisconnections.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: redisconnections.stack.zncdata.net diff --git a/config/crd/patches/cainjection_in_s3buckets.yaml b/config/crd/patches/cainjection_in_s3buckets.yaml new file mode 100644 index 0000000..a883158 --- /dev/null +++ b/config/crd/patches/cainjection_in_s3buckets.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: s3buckets.stack.zncdata.net diff --git a/config/crd/patches/cainjection_in_s3connections.yaml b/config/crd/patches/cainjection_in_s3connections.yaml new file mode 100644 index 0000000..d0287d6 --- /dev/null +++ b/config/crd/patches/cainjection_in_s3connections.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: s3connections.stack.zncdata.net diff --git a/config/crd/patches/webhook_in_redisconnections.yaml b/config/crd/patches/webhook_in_redisconnections.yaml new file mode 100644 index 0000000..c3b9df4 --- /dev/null +++ b/config/crd/patches/webhook_in_redisconnections.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: redisconnections.stack.zncdata.net +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/crd/patches/webhook_in_s3buckets.yaml b/config/crd/patches/webhook_in_s3buckets.yaml new file mode 100644 index 0000000..283effa --- /dev/null +++ b/config/crd/patches/webhook_in_s3buckets.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: s3buckets.stack.zncdata.net +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/crd/patches/webhook_in_s3connections.yaml b/config/crd/patches/webhook_in_s3connections.yaml new file mode 100644 index 0000000..81a29d8 --- /dev/null +++ b/config/crd/patches/webhook_in_s3connections.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: s3connections.stack.zncdata.net +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 5c5f0b8..ad13e96 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -1,2 +1,8 @@ resources: - manager.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: controller + newName: controller + newTag: latest diff --git a/config/manifests/bases/zncdata-stack-operator.clusterserviceversion.yaml b/config/manifests/bases/zncdata-stack-operator.clusterserviceversion.yaml new file mode 100644 index 0000000..8ee5e31 --- /dev/null +++ b/config/manifests/bases/zncdata-stack-operator.clusterserviceversion.yaml @@ -0,0 +1,68 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: '[]' + capabilities: Basic Install + name: zncdata-stack-operator.v0.0.0 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: DatabaseConnection is the Schema for the databaseconnections API + displayName: Database Connection + kind: DatabaseConnection + name: databaseconnections.stack.zncdata.net + version: v1alpha1 + - description: Database is the Schema for the databases API + displayName: Database + kind: Database + name: databases.stack.zncdata.net + version: v1alpha1 + - description: RedisConnection is the Schema for the redisconnections API + displayName: Redis Connection + kind: RedisConnection + name: redisconnections.stack.zncdata.net + version: v1alpha1 + - description: S3Bucket is the Schema for the s3buckets API + displayName: S3 Bucket + kind: S3Bucket + name: s3buckets.stack.zncdata.net + version: v1alpha1 + - description: S3Connection is the Schema for the s3connections API + displayName: S3 Connection + kind: S3Connection + name: s3connections.stack.zncdata.net + version: v1alpha1 + description: provider some special resource to other operator + displayName: zncdata-stack-operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + deployments: null + strategy: "" + installModes: + - supported: false + type: OwnNamespace + - supported: false + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces + keywords: + - base + - s3 + - database + - redis + - authentik + links: + - name: Zncdata Stack Operator + url: https://zncdata-stack-operator.domain + maturity: alpha + provider: + name: zncdata.net + version: 0.0.0 diff --git a/config/rbac/redisconnection_editor_role.yaml b/config/rbac/redisconnection_editor_role.yaml new file mode 100644 index 0000000..476314d --- /dev/null +++ b/config/rbac/redisconnection_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit redisconnections. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: redisconnection-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: zncdata-stack-operator + app.kubernetes.io/part-of: zncdata-stack-operator + app.kubernetes.io/managed-by: kustomize + name: redisconnection-editor-role +rules: +- apiGroups: + - stack.zncdata.net + resources: + - redisconnections + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - stack.zncdata.net + resources: + - redisconnections/status + verbs: + - get diff --git a/config/rbac/redisconnection_viewer_role.yaml b/config/rbac/redisconnection_viewer_role.yaml new file mode 100644 index 0000000..85258fa --- /dev/null +++ b/config/rbac/redisconnection_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view redisconnections. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: redisconnection-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: zncdata-stack-operator + app.kubernetes.io/part-of: zncdata-stack-operator + app.kubernetes.io/managed-by: kustomize + name: redisconnection-viewer-role +rules: +- apiGroups: + - stack.zncdata.net + resources: + - redisconnections + verbs: + - get + - list + - watch +- apiGroups: + - stack.zncdata.net + resources: + - redisconnections/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index df9af4a..5ee8962 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -4,6 +4,18 @@ kind: ClusterRole metadata: name: manager-role rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - stack.zncdata.net resources: @@ -56,3 +68,81 @@ rules: - get - patch - update +- apiGroups: + - stack.zncdata.net + resources: + - redisconnections + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - stack.zncdata.net + resources: + - redisconnections/finalizers + verbs: + - update +- apiGroups: + - stack.zncdata.net + resources: + - redisconnections/status + verbs: + - get + - patch + - update +- apiGroups: + - stack.zncdata.net + resources: + - s3buckets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - stack.zncdata.net + resources: + - s3buckets/finalizers + verbs: + - update +- apiGroups: + - stack.zncdata.net + resources: + - s3buckets/status + verbs: + - get + - patch + - update +- apiGroups: + - stack.zncdata.net + resources: + - s3connections + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - stack.zncdata.net + resources: + - s3connections/finalizers + verbs: + - update +- apiGroups: + - stack.zncdata.net + resources: + - s3connections/status + verbs: + - get + - patch + - update diff --git a/config/rbac/s3bucket_editor_role.yaml b/config/rbac/s3bucket_editor_role.yaml new file mode 100644 index 0000000..8b3852f --- /dev/null +++ b/config/rbac/s3bucket_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit s3buckets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: s3bucket-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: zncdata-stack-operator + app.kubernetes.io/part-of: zncdata-stack-operator + app.kubernetes.io/managed-by: kustomize + name: s3bucket-editor-role +rules: +- apiGroups: + - stack.zncdata.net + resources: + - s3buckets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - stack.zncdata.net + resources: + - s3buckets/status + verbs: + - get diff --git a/config/rbac/s3bucket_viewer_role.yaml b/config/rbac/s3bucket_viewer_role.yaml new file mode 100644 index 0000000..ea9d169 --- /dev/null +++ b/config/rbac/s3bucket_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view s3buckets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: s3bucket-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: zncdata-stack-operator + app.kubernetes.io/part-of: zncdata-stack-operator + app.kubernetes.io/managed-by: kustomize + name: s3bucket-viewer-role +rules: +- apiGroups: + - stack.zncdata.net + resources: + - s3buckets + verbs: + - get + - list + - watch +- apiGroups: + - stack.zncdata.net + resources: + - s3buckets/status + verbs: + - get diff --git a/config/rbac/s3connection_editor_role.yaml b/config/rbac/s3connection_editor_role.yaml new file mode 100644 index 0000000..d227544 --- /dev/null +++ b/config/rbac/s3connection_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit s3connections. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: s3connection-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: zncdata-stack-operator + app.kubernetes.io/part-of: zncdata-stack-operator + app.kubernetes.io/managed-by: kustomize + name: s3connection-editor-role +rules: +- apiGroups: + - stack.zncdata.net + resources: + - s3connections + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - stack.zncdata.net + resources: + - s3connections/status + verbs: + - get diff --git a/config/rbac/s3connection_viewer_role.yaml b/config/rbac/s3connection_viewer_role.yaml new file mode 100644 index 0000000..3b0b9c6 --- /dev/null +++ b/config/rbac/s3connection_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view s3connections. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: s3connection-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: zncdata-stack-operator + app.kubernetes.io/part-of: zncdata-stack-operator + app.kubernetes.io/managed-by: kustomize + name: s3connection-viewer-role +rules: +- apiGroups: + - stack.zncdata.net + resources: + - s3connections + verbs: + - get + - list + - watch +- apiGroups: + - stack.zncdata.net + resources: + - s3connections/status + verbs: + - get diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index d8a97c4..3043441 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -1,5 +1,8 @@ ## Append samples of your project ## resources: -- stack_v1alpha1_databaseconnection.yaml +- stack_v1alpha1_databaseconnection_mysql.yaml - stack_v1alpha1_database.yaml +- stack_v1alpha1_s3connection.yaml +- stack_v1alpha1_s3bucket.yaml +- stack_v1alpha1_redisconnection.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/stack_v1alpha1_database_postgres.yaml b/config/samples/stack_v1alpha1_database_postgres.yaml new file mode 100644 index 0000000..3727ee7 --- /dev/null +++ b/config/samples/stack_v1alpha1_database_postgres.yaml @@ -0,0 +1,18 @@ +apiVersion: stack.zncdata.net/v1alpha1 +kind: Database +metadata: + labels: + app.kubernetes.io/name: database + app.kubernetes.io/instance: database-sample + app.kubernetes.io/part-of: zncdata-stack-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: zncdata-stack-operator + name: database-sample +spec: + name: test-db1 + reference: databaseconnection-sample + credential: + username: zhangsan + password: zhangsan + + # TODO(user): Add fields here diff --git a/config/samples/stack_v1alpha1_databaseconnection.yaml b/config/samples/stack_v1alpha1_databaseconnection_mysql.yaml similarity index 65% rename from config/samples/stack_v1alpha1_databaseconnection.yaml rename to config/samples/stack_v1alpha1_databaseconnection_mysql.yaml index 0f7eaa3..368c396 100644 --- a/config/samples/stack_v1alpha1_databaseconnection.yaml +++ b/config/samples/stack_v1alpha1_databaseconnection_mysql.yaml @@ -7,6 +7,15 @@ metadata: app.kubernetes.io/part-of: zncdata-stack-operator app.kubernetes.io/managed-by: kustomize app.kubernetes.io/created-by: zncdata-stack-operator - name: databaseconnection-sample + name: databaseconnection-sample-mysql spec: + default: true + provider: + driver: mysql + host: localhost + port: 3306 + ssl: false + credential: + username: root + password: "123456" # TODO(user): Add fields here diff --git a/config/samples/stack_v1alpha1_databaseconnection_postgres.yaml b/config/samples/stack_v1alpha1_databaseconnection_postgres.yaml new file mode 100644 index 0000000..b1a95b5 --- /dev/null +++ b/config/samples/stack_v1alpha1_databaseconnection_postgres.yaml @@ -0,0 +1,21 @@ +apiVersion: stack.zncdata.net/v1alpha1 +kind: DatabaseConnection +metadata: + labels: + app.kubernetes.io/name: databaseconnection + app.kubernetes.io/instance: databaseconnection-sample + app.kubernetes.io/part-of: zncdata-stack-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: zncdata-stack-operator + name: databaseconnection-sample-postgres +spec: + default: true + provider: + driver: postgres + host: localhost + port: 5432 + ssl: false + credential: + username: root + password: "123456" + # TODO(user): Add fields here diff --git a/config/samples/stack_v1alpha1_redisconnection.yaml b/config/samples/stack_v1alpha1_redisconnection.yaml new file mode 100644 index 0000000..3c12b98 --- /dev/null +++ b/config/samples/stack_v1alpha1_redisconnection.yaml @@ -0,0 +1,12 @@ +apiVersion: stack.zncdata.net/v1alpha1 +kind: RedisConnection +metadata: + labels: + app.kubernetes.io/name: redisconnection + app.kubernetes.io/instance: redisconnection-sample + app.kubernetes.io/part-of: zncdata-stack-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: zncdata-stack-operator + name: redisconnection-sample +spec: + # TODO(user): Add fields here diff --git a/config/samples/stack_v1alpha1_s3bucket.yaml b/config/samples/stack_v1alpha1_s3bucket.yaml new file mode 100644 index 0000000..e7887c1 --- /dev/null +++ b/config/samples/stack_v1alpha1_s3bucket.yaml @@ -0,0 +1,14 @@ +apiVersion: stack.zncdata.net/v1alpha1 +kind: S3Bucket +metadata: + labels: + app.kubernetes.io/name: s3bucket + app.kubernetes.io/instance: s3bucket-sample + app.kubernetes.io/part-of: zncdata-stack-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: zncdata-stack-operator + name: s3bucket-sample +spec: + reference: s3connection-sample + name: "zhangsan" + # TODO(user): Add fields here diff --git a/config/samples/stack_v1alpha1_s3connection.yaml b/config/samples/stack_v1alpha1_s3connection.yaml new file mode 100644 index 0000000..6574c8e --- /dev/null +++ b/config/samples/stack_v1alpha1_s3connection.yaml @@ -0,0 +1,16 @@ +apiVersion: stack.zncdata.net/v1alpha1 +kind: S3Connection +metadata: + labels: + app.kubernetes.io/name: s3connection + app.kubernetes.io/instance: s3connection-sample + app.kubernetes.io/part-of: zncdata-stack-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: zncdata-stack-operator + name: s3connection-sample +spec: + credential: + accessKey: admin + secretKey: admin123456 + endpoint: 127.0.0.1:9000 + ssl: false diff --git a/docs/resources/hive-example.yaml b/docs/resources/hive-example.yaml index 976ee14..8b5d51e 100644 --- a/docs/resources/hive-example.yaml +++ b/docs/resources/hive-example.yaml @@ -4,12 +4,15 @@ metadata: spec: database: name: "hive_test" # db name in database + db_name: "" connection: driver: postgres host: port: credential: existingSecret: "foo" + username: + password: --- kind: HiveMetastore @@ -34,7 +37,7 @@ kind: DatabaseConnection metadata: name: pg-db-provider-xxx spec: - privider: + provider: driver: postgres host: port: @@ -50,3 +53,6 @@ spec: reference: "pg-db-provider-xxx" credential: existingSecret: "foo" + username: + password: + diff --git a/docs/resources/redis-example.yaml b/docs/resources/redis-example.yaml index 967e726..6704e87 100644 --- a/docs/resources/redis-example.yaml +++ b/docs/resources/redis-example.yaml @@ -4,16 +4,16 @@ spec: redis: host: port: - credential: - existingSecret: "foo" +# credential: +# existingSecret: "foo" --- Kind: Foo name: foo-yyy spec: redis: - name: "redis-xxx" - provider: "redis-provider-xxx" +# name: "redis-xxx" +# provider: "redis-provider-xxx" --- @@ -25,10 +25,10 @@ spec: credential: existingSecret: "foo" ---- -Kind: Redis -name: redis-xxx -spec: - provider: "redis-provider-xxx" - credential: - existingSecret: "foo" +#--- +#Kind: Redis +#name: redis-xxx +#spec: +# provider: "redis-provider-xxx" +# credential: +# existingSecret: "foo" diff --git a/docs/resources/s3-example.yaml b/docs/resources/s3-example.yaml index 1d39506..304ebe5 100644 --- a/docs/resources/s3-example.yaml +++ b/docs/resources/s3-example.yaml @@ -30,18 +30,30 @@ spec: reference: "s3-bucket-xxx" --- -kiind: S3Connection +kind: S3Connection metadata: name: s3-connection-xxx spec: - host: - port: + credential: + existingSecret: "pg-root" + endpoint: + accessKey: + secretKey: + region: + ssl: +# Endpoint string `json:"endpoint,omitempty"` +# AccessKey string `json:"access_key,omitempty"` +# SecretKey string `json:"secret_key,omitempty"` +# Region string `json:"region,omitempty"` +# Token string `json:"token,omitempty"` +# SSL bool `json:"ssl,omitempty"` --- kind: S3Bucket metadata: name: trino-test spec: name: "trino-test" - connection: - reference: "s3-connection-xxx" + reference: "s3-connection-xxx" + credential: + existingSecret: "pg-root" diff --git a/go.mod b/go.mod index 10942f0..5dc7180 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,49 @@ module github.com/zncdata-labs/zncdata-stack-operator go 1.19 require ( + github.com/cisco-open/k8s-objectmatcher v1.9.0 github.com/joho/godotenv v1.5.1 + github.com/minio/madmin-go/v3 v3.0.35 + github.com/minio/minio-go/v7 v7.0.65 github.com/onsi/ginkgo/v2 v2.13.0 github.com/onsi/gomega v1.29.0 - k8s.io/apimachinery v0.28.3 - k8s.io/client-go v0.28.3 + github.com/redis/go-redis/v9 v9.3.0 + k8s.io/apimachinery v0.28.4 + k8s.io/client-go v0.28.4 + k8s.io/kubectl v0.28.4 sigs.k8s.io/controller-runtime v0.16.3 ) -require github.com/pmezard/go-difflib v1.0.0 // indirect +require ( + emperror.dev/errors v0.8.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/evanphx/json-patch v5.6.0+incompatible // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/klauspost/compress v1.16.7 // indirect + github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/philhofer/fwd v1.1.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect + github.com/prometheus/prom2json v1.3.3 // indirect + github.com/rs/xid v1.5.0 // indirect + github.com/safchain/ethtool v0.3.0 // indirect + github.com/secure-io/sio-go v0.3.1 // indirect + github.com/shirou/gopsutil/v3 v3.23.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tinylib/msgp v1.1.8 // indirect + github.com/tklauser/go-sysconf v0.3.11 // indirect + github.com/tklauser/numcpus v0.6.0 // indirect + github.com/yusufpapurcu/wmi v1.2.2 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/sync v0.4.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect +) replace github.com/onsi/ginkgo/v2 => github.com/onsi/ginkgo/v2 v2.12.0 @@ -22,7 +56,7 @@ require ( github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/go-logr/logr v1.3.0 // indirect + github.com/go-logr/logr v1.3.0 github.com/go-logr/zapr v1.2.4 // indirect github.com/go-openapi/jsonpointer v0.20.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect @@ -69,9 +103,9 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.28.3 // indirect + k8s.io/api v0.28.4 k8s.io/apiextensions-apiserver v0.28.3 // indirect - k8s.io/component-base v0.28.3 // indirect + k8s.io/component-base v0.28.4 // indirect k8s.io/klog/v2 v2.100.1 // indirect k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect diff --git a/go.sum b/go.sum index 79b14ec..f06a6de 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,29 @@ +emperror.dev/errors v0.8.1 h1:UavXZ5cSX/4u9iyvH6aDcuGkVjeexUGJ7Ij7G4VfQT0= +emperror.dev/errors v0.8.1/go.mod h1:YcRvLPh626Ubn2xqtoprejnA5nFha+TJ+2vew48kWuE= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cisco-open/k8s-objectmatcher v1.9.0 h1:/sfuO0BD09fpynZjXsqeZrh28Juc4VEwc2P6Ov/Q6fM= +github.com/cisco-open/k8s-objectmatcher v1.9.0/go.mod h1:CH4E6qAK+q+JwKFJn0DaTNqxrbmWCaDQzGthKLK4nZ0= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc= github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -23,6 +34,8 @@ github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= @@ -37,8 +50,11 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEe github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= @@ -46,6 +62,7 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 h1:0VpGH+cDhbDtdcweoyCVsF3fhN8kejK6rFe/2FFX2nU= github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49/go.mod h1:BkkQ4L1KS1xMt2aWSPStnn55ChGC0DPOn2FQYj+f25M= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -67,6 +84,11 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= +github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -76,10 +98,23 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de h1:V53FWzU6KAZVi1tPp5UIsMoUWJ2/PNwYIDXnu7QuBCE= +github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/minio/madmin-go/v3 v3.0.35 h1:cCo5ZZpHA+rlBQbsAcwFwiuh/uHJmjVoDDx1G4+zaho= +github.com/minio/madmin-go/v3 v3.0.35/go.mod h1:4QN2NftLSV7MdlT50dkrenOMmNVHluxTvlqJou3hte8= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.65 h1:sOlB8T3nQK+TApTpuN3k4WD5KasvZIE3vVFzyyCa0go= +github.com/minio/minio-go/v7 v7.0.65/go.mod h1:R4WVUR6ZTedlCcGwZRauLMIKjgyaWxhs4Mqi/OMPmEc= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -91,11 +126,17 @@ github.com/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI github.com/onsi/ginkgo/v2 v2.12.0/go.mod h1:ZNEzXISYlqpb8S36iN71ifqLi3vVD1rVJGvWRCJOUpQ= github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= +github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig= +github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= @@ -104,7 +145,21 @@ github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lne github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/prom2json v1.3.3 h1:IYfSMiZ7sSOfliBoo89PcufjWO4eAR0gznGcETyaUgo= +github.com/prometheus/prom2json v1.3.3/go.mod h1:Pv4yIPktEkK7btWsrUTWDDDrnpUrAELaOCj+oFwlgmc= +github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0= +github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= +github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= +github.com/secure-io/sio-go v0.3.1 h1:dNvY9awjabXTYGsTF1PiCySl9Ltofk9GA3VdWlo7rRc= +github.com/secure-io/sio-go v0.3.1/go.mod h1:+xbkjDzPjwh4Axd07pRKSNriS9SCiYksWnZqdnfpQxs= +github.com/shirou/gopsutil/v3 v3.23.1 h1:a9KKO+kGLKEvcPIs4W62v0nu3sciVDOOOPUD0Hz7z/4= +github.com/shirou/gopsutil/v3 v3.23.1/go.mod h1:NN6mnm5/0k8jw4cBfCnJtr5L7ErOTg18tMNpgFkn0hA= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -118,10 +173,18 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= +github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= +github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= +github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= +github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= +github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= +github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= @@ -133,8 +196,11 @@ go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -142,6 +208,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -151,35 +218,51 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= @@ -191,6 +274,7 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -211,26 +295,30 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM= -k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc= +k8s.io/api v0.28.4 h1:8ZBrLjwosLl/NYgv1P7EQLqoO8MGQApnbgH8tu3BMzY= +k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0= k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08= k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc= -k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A= -k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8= -k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4= -k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo= -k8s.io/component-base v0.28.3 h1:rDy68eHKxq/80RiMb2Ld/tbH8uAE75JdCqJyi6lXMzI= -k8s.io/component-base v0.28.3/go.mod h1:fDJ6vpVNSk6cRo5wmDa6eKIG7UlIQkaFmZN2fYgIUD8= +k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8= +k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg= +k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY= +k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4= +k8s.io/component-base v0.28.4 h1:c/iQLWPdUgI90O+T9TeECg8o7N3YJTiuz2sKxILYcYo= +k8s.io/component-base v0.28.4/go.mod h1:m9hR0uvqXDybiGL2nf/3Lf0MerAfQXzkfWhUY58JUbU= k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/kubectl v0.28.4 h1:gWpUXW/T7aFne+rchYeHkyB8eVDl5UZce8G4X//kjUQ= +k8s.io/kubectl v0.28.4/go.mod h1:CKOccVx3l+3MmDbkXtIUtibq93nN2hkDR99XDCn7c/c= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= diff --git a/internal/controller/database/database_controller.go b/internal/controller/database/database_controller.go index 17d39d4..b3ba6ed 100644 --- a/internal/controller/database/database_controller.go +++ b/internal/controller/database/database_controller.go @@ -18,19 +18,27 @@ package controller import ( "context" - + "encoding/base64" + "errors" + "github.com/go-logr/logr" + stackv1alpha1 "github.com/zncdata-labs/zncdata-stack-operator/api/v1alpha1" + "github.com/zncdata-labs/zncdata-stack-operator/internal/util" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/util/retry" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" - - stackv1alpha1 "github.com/zncdata-labs/zncdata-stack-operator/api/v1alpha1" + "strings" ) // DatabaseReconciler reconciles a Database object type DatabaseReconciler struct { client.Client Scheme *runtime.Scheme + Log logr.Logger } //+kubebuilder:rbac:groups=stack.zncdata.net,resources=databases,verbs=get;list;watch;create;update;patch;delete @@ -49,14 +57,216 @@ type DatabaseReconciler struct { func (r *DatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { _ = log.FromContext(ctx) - // TODO(user): your logic here + obj := &stackv1alpha1.Database{} + // 1. Get the Database object + if err := r.Get(ctx, req.NamespacedName, obj); err != nil { + if err := client.IgnoreNotFound(err); err != nil { + r.Log.Error(err, "unable to fetch Database") + return ctrl.Result{}, err + } + r.Log.Info("Database not found, Ignoring it since it has been deleted") + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // 2. Get the DatabaseConnection object, if it exists; use the default if it doesn't exist. + connection, err := r.getReferenceDatabaseConnection(ctx, obj) + if err != nil { + r.Log.Error(err, "unable to fetch DatabaseConnection") + return ctrl.Result{}, err + } + + // 3. Get the DatabaseSecret object, if it exists; use the default if it doesn't exist. + err = r.generateDatabaseSecret(ctx, obj, connection) + if err != nil { + r.Log.Error(err, "unable to generate DatabaseSecret") + return ctrl.Result{}, err + } + r.Log.Info("DatabaseSecret generated") + + isDatabaseMarkedToBeDeleted := obj.GetDeletionTimestamp() != nil + if isDatabaseMarkedToBeDeleted { + if controllerutil.ContainsFinalizer(obj, stackv1alpha1.DatabaseFinalizer) { + if err := r.finalizeDatabase(ctx, obj); err != nil { + return ctrl.Result{}, err + } + controllerutil.RemoveFinalizer(obj, stackv1alpha1.DatabaseFinalizer) + if err := r.Update(ctx, obj); err != nil { + return ctrl.Result{}, err + } + } + return ctrl.Result{}, nil + } + + // Add finalizer for this CR + if !controllerutil.ContainsFinalizer(obj, stackv1alpha1.DatabaseFinalizer) { + controllerutil.AddFinalizer(obj, stackv1alpha1.DatabaseFinalizer) + err = r.Update(ctx, obj) + if err != nil { + return ctrl.Result{}, err + } + } + + if obj.IsAvailable() { + return ctrl.Result{}, nil + } + obj.SetStatusCondition(metav1.Condition{ + Type: stackv1alpha1.ConditionTypeAvailable, + Status: metav1.ConditionTrue, + Reason: stackv1alpha1.ConditionReasonRunning, + Message: "DatabaseConnection is running", + ObservedGeneration: obj.GetGeneration(), + }) + + if err := r.UpdateStatus(ctx, obj); err != nil { + r.Log.Error(err, "Failed to update status") + return ctrl.Result{}, err + } return ctrl.Result{}, nil } +func (r *DatabaseReconciler) UpdateStatus(ctx context.Context, instance *stackv1alpha1.Database) error { + retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { + return r.Status().Update(ctx, instance) + }) + if retryErr != nil { + r.Log.Error(retryErr, "Failed to update vfm status after retries") + return retryErr + } + r.Log.V(1).Info("Successfully patched object status") + return nil +} + // SetupWithManager sets up the controller with the Manager. func (r *DatabaseReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&stackv1alpha1.Database{}). Complete(r) } + +func (r *DatabaseReconciler) getReferenceDatabaseConnection(ctx context.Context, obj *stackv1alpha1.Database) (*stackv1alpha1.DatabaseConnection, error) { + if obj.Spec.Reference != "" { + connection := &stackv1alpha1.DatabaseConnection{} + err := r.Get(ctx, client.ObjectKey{Namespace: obj.Namespace, Name: obj.Spec.Reference}, connection) + if err != nil { + return nil, err + } + return connection, nil + } + return nil, errors.New("no reference found") +} + +func (r *DatabaseReconciler) generateDatabaseSecret(ctx context.Context, obj *stackv1alpha1.Database, connection *stackv1alpha1.DatabaseConnection) error { + // 如果可用,或者要删除了,就不再创建 + if obj.IsAvailable() || obj.GetDeletionTimestamp() != nil || controllerutil.ContainsFinalizer(obj, stackv1alpha1.DatabaseFinalizer) { + return nil + } + dbName := obj.Spec.Name + username := strings.ToLower(util.RemoveSpecialCharacter(obj.GetName() + util.GenerateRandomStr(5))) + password := util.GenerateRandomStr(10) + dsn, err := getDSNFromConnection(ctx, r.Client, connection) + if err != nil { + return err + } + initializer, err := NewDBInitializer(dsn) + if err != nil { + return err + } + // 初始化用户 + if err = initializer.initUser(username, password); err != nil { + return err + } + // 初始化数据库 + if err = initializer.initDatabase(username, dbName); err != nil { + return err + } + + // 测试生成的数据库连通性 + newDsn := &DSN{ + Host: dsn.Host, + Port: dsn.Port, + Driver: dsn.Driver, + SSLMode: dsn.SSLMode, + Username: username, + Password: password, + Database: dbName, + } + + dbInitializer, err := NewDBInitializer(newDsn) + if err != nil { + return err + } + // 检测连通性 + err = dbInitializer.ping() + if err != nil { + return err + } + // 创建secret + data := make(map[string][]byte) + data["username"] = []byte(username) + data["password"] = []byte(password) + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: obj.GetNameWithSuffix("secret"), + Namespace: obj.Namespace, + }, + Type: corev1.SecretTypeOpaque, + Data: data, + } + err = util.CreateOrUpdate(ctx, r.Client, secret) + if err != nil { + return err + } + return nil +} + +func (r *DatabaseReconciler) finalizeDatabase(ctx context.Context, obj *stackv1alpha1.Database) error { + + // 1. 获取secret + reference := obj.Spec.Reference + connection := &stackv1alpha1.DatabaseConnection{} + err := r.Get(ctx, client.ObjectKey{Namespace: obj.Namespace, Name: reference}, connection) + if err != nil { + return err + } + adminDSN, err := getDSNFromConnection(ctx, r.Client, connection) + if err != nil { + return err + } + + admin, err := NewDBInitializer(adminDSN) + if err != nil { + return err + } + err = admin.dropDatabase(obj.Spec.Name) + if err != nil { + return err + } + + username := obj.Spec.Credential.Username + if obj.Spec.Credential.ExistSecret != "" { + secret := &corev1.Secret{} + err = r.Get(ctx, client.ObjectKey{Namespace: obj.Namespace, Name: obj.Spec.Credential.ExistSecret}, secret) + if err != nil { + return err + } + if ub, ok := secret.Data["username"]; ok { + decodeString, err := base64.StdEncoding.DecodeString(string(ub)) + if err != nil { + return err + } + username = string(decodeString) + } + + err = r.Delete(ctx, secret) + if err != nil { + return err + } + } + err = admin.dropUser(username) + if err != nil { + return err + } + return nil +} diff --git a/internal/controller/database/databaseconnection_controller.go b/internal/controller/database/databaseconnection_controller.go index 227d7e1..2415af1 100644 --- a/internal/controller/database/databaseconnection_controller.go +++ b/internal/controller/database/databaseconnection_controller.go @@ -18,8 +18,12 @@ package controller import ( "context" - + "errors" + "github.com/go-logr/logr" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/util/retry" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" @@ -31,11 +35,13 @@ import ( type DatabaseConnectionReconciler struct { client.Client Scheme *runtime.Scheme + Log logr.Logger } //+kubebuilder:rbac:groups=stack.zncdata.net,resources=databaseconnections,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=stack.zncdata.net,resources=databaseconnections/status,verbs=get;update;patch //+kubebuilder:rbac:groups=stack.zncdata.net,resources=databaseconnections/finalizers,verbs=update +// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -49,7 +55,68 @@ type DatabaseConnectionReconciler struct { func (r *DatabaseConnectionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { _ = log.FromContext(ctx) - // TODO(user): your logic here + databaseConnection := &stackv1alpha1.DatabaseConnection{} + + if err := r.Get(ctx, req.NamespacedName, databaseConnection); err != nil { + if client.IgnoreNotFound(err) != nil { + r.Log.Error(err, "unable to fetch instance") + return ctrl.Result{}, err + } + r.Log.Info("DatabaseConnection resource not found. Ignoring since object must be deleted") + return ctrl.Result{}, nil + } + + //// Get the status condition, if it exists and its generation is not the + ////same as the DatabaseConnection's generation, reset the status conditions + readCondition := apimeta.FindStatusCondition(databaseConnection.Status.Conditions, stackv1alpha1.ConditionTypeProgressing) + if readCondition == nil || readCondition.ObservedGeneration != databaseConnection.GetGeneration() { + databaseConnection.InitStatusConditions() + if err := r.UpdateStatus(ctx, databaseConnection); err != nil { + return ctrl.Result{}, err + } + } + + r.Log.Info("DatabaseConnection is found", "Name", databaseConnection.Name) + + if err := r.checkDefault(ctx, databaseConnection); err != nil { + r.Log.Error(err, "Failed to check default") + databaseConnection.SetStatusCondition(metav1.Condition{ + Type: stackv1alpha1.ConditionTypeReconcile, + Status: metav1.ConditionFalse, + Reason: stackv1alpha1.ConditionReasonPreparing, + Message: "DatabaseConnection has exist default.", + ObservedGeneration: databaseConnection.GetGeneration(), + }) + if err := r.UpdateStatus(ctx, databaseConnection); err != nil { + r.Log.Error(err, "Failed to update status") + return ctrl.Result{}, err + } + return ctrl.Result{}, err + } + + if err := r.checkConnection(ctx, databaseConnection); err != nil { + r.Log.Error(err, "Failed to check connection") + return ctrl.Result{}, err + } + + if databaseConnection.IsAvailable() { + return ctrl.Result{}, nil + } + + databaseConnection.SetStatusCondition(metav1.Condition{ + Type: stackv1alpha1.ConditionTypeAvailable, + Status: metav1.ConditionTrue, + Reason: stackv1alpha1.ConditionReasonRunning, + Message: "DatabaseConnection is running", + ObservedGeneration: databaseConnection.GetGeneration(), + }) + + if err := r.UpdateStatus(ctx, databaseConnection); err != nil { + r.Log.Error(err, "Failed to update status") + return ctrl.Result{}, err + } + + r.Log.Info("Successfully updated status") return ctrl.Result{}, nil } @@ -60,3 +127,37 @@ func (r *DatabaseConnectionReconciler) SetupWithManager(mgr ctrl.Manager) error For(&stackv1alpha1.DatabaseConnection{}). Complete(r) } + +func (r *DatabaseConnectionReconciler) UpdateStatus(ctx context.Context, instance *stackv1alpha1.DatabaseConnection) error { + retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { + return r.Status().Update(ctx, instance) + }) + if retryErr != nil { + r.Log.Error(retryErr, "Failed to update vfm status after retries") + return retryErr + } + r.Log.V(1).Info("Successfully patched object status") + return nil +} + +func (r *DatabaseConnectionReconciler) checkDefault(ctx context.Context, connection *stackv1alpha1.DatabaseConnection) error { + if !connection.Spec.Default { + return nil + } + list := stackv1alpha1.DatabaseConnectionList{} + ops := &client.ListOptions{ + Namespace: connection.Namespace, + } + err := r.Client.List(ctx, &list, ops) + if err != nil { + return err + } + for _, item := range list.Items { // 同一个driver类型的default只能有一个 + if item.Spec.Default && + item.Name != connection.Name && + item.Spec.Provider.Driver == connection.Spec.Provider.Driver { + return errors.New("default connection already exists") + } + } + return nil +} diff --git a/internal/controller/database/db_init.go b/internal/controller/database/db_init.go index 8e47fda..c14f667 100644 --- a/internal/controller/database/db_init.go +++ b/internal/controller/database/db_init.go @@ -2,26 +2,67 @@ package controller import ( "database/sql" - "strconv" + "fmt" + _ "github.com/go-sql-driver/mysql" + _ "github.com/lib/pq" + "strings" ) type DSN struct { Driver string Host string - Port int + Port string SSLMode bool Username string Password string + Database string } -func (d *DSN) String() string { +func (d *DSN) mysqlString() string { + if d.Database == "" { + d.Database = "mysql" + } + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", d.Username, d.Password, d.Host, d.Port, d.Database) + return dsn +} + +func (d *DSN) postgresString() string { var sslMode string if d.SSLMode { sslMode = "require" } else { sslMode = "disable" } - return "host=" + d.Host + " port=" + strconv.Itoa(d.Port) + " user=" + d.Username + " password=" + d.Password + " sslmode=" + sslMode + builder := strings.Builder{} + + if d.Host != "" { + builder.WriteString("host=" + d.Host) + } + if d.Port != "" { + builder.WriteString(" port=" + d.Port) + } + if d.Username != "" { + builder.WriteString(" user=" + d.Username) + } + if d.Password != "" { + builder.WriteString(" password=" + d.Password) + } + if d.Database != "" { + builder.WriteString(" dbname=" + d.Database) + } + + builder.WriteString(" sslmode=" + sslMode) + + return builder.String() +} +func (d *DSN) String() string { + switch d.Driver { + case "mysql": + return d.mysqlString() + case "postgres": + return d.postgresString() + } + return "" } type IDBInitializer interface { @@ -30,6 +71,7 @@ type IDBInitializer interface { dropDatabase(dbname string) error dropUser(username string) error setConnection(conn *sql.DB) + ping() error } type DBInitializer struct { @@ -58,6 +100,10 @@ func (d *DBInitializer) setConnection(conn *sql.DB) { d.conn = conn } +func (d *DBInitializer) ping() error { + return d.conn.Ping() +} + func NewDBInitializer(dsn *DSN) (IDBInitializer, error) { var initializer IDBInitializer @@ -71,11 +117,9 @@ func NewDBInitializer(dsn *DSN) (IDBInitializer, error) { } dsnString := dsn.String() db, err := OpenDB(dsn.Driver, &dsnString) - db.Ping() if err != nil { return nil, err } - initializer.setConnection(db) return initializer, nil @@ -94,16 +138,31 @@ type PostgresInitializer struct { } func (d *PostgresInitializer) initDatabase(username string, dbname string) error { - _, err := d.conn.Exec("CREATE DATABASE " + dbname + " OWNER " + username) + // 查询数据库是否已经存在 + + query := "SELECT count(*) FROM pg_database WHERE datname = $1" + var count int + err := d.conn.QueryRow(query, dbname).Scan(&count) if err != nil { return err } + + // 判断数据库是否存在 + if count == 0 { + _, err := d.conn.Exec("CREATE DATABASE " + dbname) + if err != nil { + return err + } + } + _, err = d.conn.Exec("GRANT ALL PRIVILEGES ON DATABASE " + dbname + " TO " + username) return err } func (d *PostgresInitializer) initUser(username string, password string) error { - _, err := d.conn.Exec("CREATE USER " + username + " WITH PASSWORD '" + password + "'") + execSql := "CREATE USER " + username + " WITH PASSWORD '" + password + "'" + _, err := d.conn.Exec(execSql) + return err } @@ -112,15 +171,28 @@ type MySQLInitializer struct { } func (d *MySQLInitializer) initDatabase(username string, dbname string) error { - _, err := d.conn.Exec("CREATE DATABASE " + dbname) + + query := fmt.Sprintf("SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '%s'", dbname) + var count int + err := d.conn.QueryRow(query).Scan(&count) if err != nil { return err } + // 判断数据库是否存在 + if count > 0 { + fmt.Println("数据库已存在") + } else { + _, err := d.conn.Exec("CREATE DATABASE " + dbname) + if err != nil { + return err + } + } _, err = d.conn.Exec("GRANT ALL PRIVILEGES ON " + dbname + ".* TO " + username) return err } func (d *MySQLInitializer) initUser(username string, password string) error { - _, err := d.conn.Exec("CREATE USER " + username + " IDENTIFIED BY " + password) + createUserSql := fmt.Sprintf("CREATE USER '%s'@'%%' IDENTIFIED BY '%s';", username, password) + _, err := d.conn.Exec(createUserSql) return err } diff --git a/internal/controller/database/db_init_test.go b/internal/controller/database/db_init_test.go index 8381a21..45a7b32 100644 --- a/internal/controller/database/db_init_test.go +++ b/internal/controller/database/db_init_test.go @@ -1,7 +1,9 @@ package controller import ( + "github.com/zncdata-labs/zncdata-stack-operator/internal/util" "os" + "strings" "testing" _ "github.com/go-sql-driver/mysql" @@ -10,16 +12,76 @@ import ( "github.com/stretchr/testify/assert" ) -var () +const ( + DRIVER_POSTGRES = "postgres" + DRIVER_MYSQL = "mysql" +) func getPgDSN() *DSN { + getenv := os.Getenv("ENV") + if getenv == "local" { + return getLocalPgDSN() + } + return getEnvDSN(DRIVER_POSTGRES) +} +func getMysqlDSN() *DSN { + getenv := os.Getenv("ENV") + if getenv == "local" { + return getLocalMysqlDSN() + } + return getEnvDSN(DRIVER_MYSQL) +} + +func getLocalPgDSN() *DSN { return &DSN{ - Driver: "postgres", - Host: os.Getenv("POSTGRES_HOST"), - Port: 5432, + Driver: DRIVER_POSTGRES, + Host: "127.0.0.1", + Port: "5432", SSLMode: false, - Username: "test", - Password: "test", + Username: "root", + Password: "123456", + } +} +func getLocalMysqlDSN() *DSN { + return &DSN{ + Driver: DRIVER_MYSQL, + Host: "127.0.0.1", + Port: "3306", + SSLMode: false, + Username: "root", + Password: "123456", + Database: "mysql", + } +} +func getEnvDSN(driverName string) *DSN { + switch driverName { + case DRIVER_POSTGRES: + return &DSN{ + Driver: DRIVER_POSTGRES, + Host: os.Getenv("PG_DB_HOST"), + Port: os.Getenv("PG_DB_PORT"), + SSLMode: os.Getenv("PG_DB_SSLMODE") == "true", + Username: os.Getenv("PG_DB_USERNAME"), + Password: os.Getenv("PG_DB_PASSWORD"), + } + case DRIVER_MYSQL: + return &DSN{ + Driver: DRIVER_MYSQL, + Host: os.Getenv("MYSQL_DB_HOST"), + Port: os.Getenv("MYSQL_DB_PORT"), + SSLMode: os.Getenv("MYSQL_DB_SSLMODE") == "true", + Username: os.Getenv("MYSQL_DB_USERNAME"), + Password: os.Getenv("MYSQL_DB_PASSWORD"), + } + + } + return &DSN{ + Driver: os.Getenv("DB_DRIVER"), + Host: os.Getenv("DB_HOST"), + Port: os.Getenv("DB_PORT"), + SSLMode: os.Getenv("DB_SSLMODE") == "true", + Username: os.Getenv("DB_USERNAME"), + Password: os.Getenv("DB_PASSWORD"), } } @@ -53,3 +115,78 @@ func TestPostgresInitializer_initUser(t *testing.T) { assert.NoError(t, err, "Expected no error") }) } + +func Test_Postgres(t *testing.T) { + initializer, err := NewDBInitializer(getPgDSN()) + if err != nil { + t.Error("new db initializer", "err: ", err) + } + t.Log("new db initializer", "initializer: ", initializer) + + randomStr := strings.ToLower(util.GenerateRandomStr(4)) + username := "test_user" + randomStr + password := "test_password" + dbname := "test_db" + randomStr + + err = initializer.initUser(username, password) + if err != nil { + t.Error("init user", "err: ", err) + } + t.Log("init user", "username: ", username, "password: ", password) + + err = initializer.initDatabase(username, dbname) + if err != nil { + t.Error("init database", "err: ", err) + } + t.Log("init database", "username: ", "test_user", "dbname: ", "test_db") + + err = initializer.dropDatabase(dbname) + if err != nil { + t.Error("drop database", "err: ", err) + } + t.Log("drop database", "dbname: ", dbname) + + err = initializer.dropUser("test_user") + if err != nil { + t.Error("drop user", "err: ", err) + } + t.Log("drop user", "username: ", "test_user") + +} + +func Test_Mysql(t *testing.T) { + initializer, err := NewDBInitializer(getMysqlDSN()) + if err != nil { + t.Error("new db initializer", "err: ", err) + } + t.Log("new db initializer", "initializer: ", initializer) + + username := "test_user1111" + password := "test_password1111" + dbname := "test_db1111" + + err = initializer.initUser(username, password) + if err != nil { + t.Error("init user", "err: ", err) + } + t.Log("init user", "username: ", username, "password: ", password) + + err = initializer.initDatabase(username, dbname) + if err != nil { + t.Error("init database", "err: ", err) + } + t.Log("init database", "username: ", username, "dbname: ", dbname) + + err = initializer.dropDatabase(dbname) + if err != nil { + t.Error("drop database", "err: ", err) + } + t.Log("drop database", "dbname: ", dbname) + + err = initializer.dropUser(username) + if err != nil { + t.Error("drop user", "err: ", err) + } + t.Log("drop user", "username: ", username) + +} diff --git a/internal/controller/database/handler.go b/internal/controller/database/handler.go new file mode 100644 index 0000000..842a6ef --- /dev/null +++ b/internal/controller/database/handler.go @@ -0,0 +1,64 @@ +package controller + +import ( + "context" + "encoding/base64" + stackv1alpha1 "github.com/zncdata-labs/zncdata-stack-operator/api/v1alpha1" + corev1 "k8s.io/api/core/v1" + apitypes "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "strconv" +) + +// 测试连接是否可用 +func (r *DatabaseConnectionReconciler) checkConnection(ctx context.Context, instance *stackv1alpha1.DatabaseConnection) error { + dsn, err := getDSNFromConnection(ctx, r.Client, instance) + if err != nil { + return err + } + initializer, err := NewDBInitializer(dsn) + if err != nil { + return err + } + return initializer.ping() +} + +func getDSNFromConnection(ctx context.Context, c client.Client, instance *stackv1alpha1.DatabaseConnection) (*DSN, error) { + provider := instance.Spec.Provider + // 查询secret + dsn := &DSN{ + Driver: provider.Driver, + Host: provider.Host, + Port: strconv.Itoa(provider.Port), + SSLMode: provider.SSL, + } + + if instance.Spec.Provider.Credential.ExistSecret != "" { + secret := &corev1.Secret{} + name := apitypes.NamespacedName{ + Namespace: instance.Namespace, + Name: instance.Spec.Provider.Credential.ExistSecret, + } + if err := c.Get(ctx, name, secret); err != nil { + if client.IgnoreNotFound(err) != nil { + return nil, err + } + } + // data base64 decode + if username, ok := secret.Data["username"]; ok { + decodeString, err := base64.StdEncoding.DecodeString(string(username)) + if err != nil { + return nil, err + } + dsn.Username = string(decodeString) + } + if password, ok := secret.Data["password"]; ok { + decodeString, err := base64.StdEncoding.DecodeString(string(password)) + if err != nil { + return nil, err + } + dsn.Password = string(decodeString) + } + } + return dsn, nil +} diff --git a/internal/controller/redis/redis_init.go b/internal/controller/redis/redis_init.go new file mode 100644 index 0000000..8ef0d6e --- /dev/null +++ b/internal/controller/redis/redis_init.go @@ -0,0 +1,45 @@ +package redis + +import ( + "context" + "github.com/redis/go-redis/v9" + "time" +) + +type IRedisConnector interface { + CheckConnection() (bool, error) +} +type Connector struct { + client *redis.Client +} + +func (r Connector) CheckConnection() (bool, error) { + _, err := r.client.Ping(context.Background()).Result() + if err != nil { + return false, err + } + return true, nil +} + +type Config struct { + Username string + Password string + Addr string +} + +func NewRedisConnector(config *Config) *Connector { + client := redis.NewClient(&redis.Options{ + Addr: config.Addr, + Username: config.Username, + Password: config.Password, + DB: 0, + DialTimeout: 1 * time.Minute, + WriteTimeout: 1 * time.Minute, + ReadTimeout: 10 * time.Second, + }) + + redisConnector := &Connector{ + client: client, + } + return redisConnector +} diff --git a/internal/controller/redis/redis_init_test.go b/internal/controller/redis/redis_init_test.go new file mode 100644 index 0000000..800ce90 --- /dev/null +++ b/internal/controller/redis/redis_init_test.go @@ -0,0 +1,43 @@ +package redis + +import ( + "os" + "testing" +) + +func NewConfigFromEnv() *Config { + username := os.Getenv("REDIS_USERNAME") + password := os.Getenv("REDIS_PASSWORD") + addr := os.Getenv("REDIS_ADDR") + + return &Config{ + Username: username, + Password: password, + Addr: addr, + } +} +func NewLocalConfig() *Config { + return &Config{ + Addr: "127.0.0.1:6379", + Password: "123456", + } +} + +func getRedisConfig() *Config { + getenv := os.Getenv("ENV") + if getenv == "local" { + return NewLocalConfig() + } + return NewConfigFromEnv() +} + +func TestNewRDBClient(t *testing.T) { + //env := NewConfigFromEnv() + env := NewLocalConfig() + connector := NewRedisConnector(env) + connection, err := connector.CheckConnection() + if err != nil { + t.Error("connect redis", "err: ", err) + } + t.Log("connect redis", "check connection result: ", connection) +} diff --git a/internal/controller/redis/redisconnection_controller.go b/internal/controller/redis/redisconnection_controller.go new file mode 100644 index 0000000..fa707b3 --- /dev/null +++ b/internal/controller/redis/redisconnection_controller.go @@ -0,0 +1,133 @@ +/* +Copyright 2023 zncdata-labs. + +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 redis + +import ( + "context" + "errors" + "fmt" + "github.com/go-logr/logr" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/retry" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + stackv1alpha1 "github.com/zncdata-labs/zncdata-stack-operator/api/v1alpha1" +) + +// RedisConnectionReconciler reconciles a RedisConnection object +type RedisConnectionReconciler struct { + client.Client + Scheme *runtime.Scheme + Log logr.Logger +} + +//+kubebuilder:rbac:groups=stack.zncdata.net,resources=redisconnections,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=stack.zncdata.net,resources=redisconnections/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=stack.zncdata.net,resources=redisconnections/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the RedisConnection object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.1/pkg/reconcile +func (r *RedisConnectionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + redisConnection := &stackv1alpha1.RedisConnection{} + if err := r.Get(ctx, req.NamespacedName, redisConnection); err != nil { + if client.IgnoreNotFound(err) != nil { + r.Log.Error(err, "unable to fetch instance") + return ctrl.Result{}, err + } + r.Log.Info("RedisConnection resource not found. Ignoring since object must be deleted") + return ctrl.Result{}, nil + } + + //// Get the status condition, if it exists and its generation is not the + ////same as the redisConnection's generation, reset the status conditions + readCondition := apimeta.FindStatusCondition(redisConnection.Status.Conditions, stackv1alpha1.ConditionTypeProgressing) + if readCondition == nil || readCondition.ObservedGeneration != redisConnection.GetGeneration() { + redisConnection.InitStatusConditions() + if err := r.UpdateStatus(ctx, redisConnection); err != nil { + return ctrl.Result{}, err + } + } + + r.Log.Info("redisConnection is found", "Name", redisConnection.Name) + + connector := NewRedisConnector(&Config{ + Addr: fmt.Sprintf("%s:%s", redisConnection.Spec.Host, redisConnection.Spec.Port), + Password: redisConnection.Spec.Password, + }) + connection, err := connector.CheckConnection() + if err != nil { + r.Log.Error(err, "connect redis", "err: ", err) + return ctrl.Result{}, err + } + if !connection { + return ctrl.Result{}, errors.New("connect redis failed") + } + + if redisConnection.IsAvailable() { + return ctrl.Result{}, err + } + + redisConnection.SetStatusCondition(metav1.Condition{ + Type: stackv1alpha1.ConditionTypeAvailable, + Status: metav1.ConditionTrue, + Reason: stackv1alpha1.ConditionReasonRunning, + Message: "DatabaseConnection is running", + ObservedGeneration: redisConnection.GetGeneration(), + }) + + if err := r.UpdateStatus(ctx, redisConnection); err != nil { + r.Log.Error(err, "Failed to update status") + return ctrl.Result{}, err + } + + r.Log.Info("Successfully updated status") + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *RedisConnectionReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&stackv1alpha1.RedisConnection{}). + Complete(r) +} + +func (r *RedisConnectionReconciler) UpdateStatus(ctx context.Context, instance *stackv1alpha1.RedisConnection) error { + retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { + return r.Status().Update(ctx, instance) + }) + if retryErr != nil { + r.Log.Error(retryErr, "Failed to update vfm status after retries") + return retryErr + } + r.Log.V(1).Info("Successfully patched object status") + return nil +} diff --git a/internal/controller/s3/handler.go b/internal/controller/s3/handler.go new file mode 100644 index 0000000..7294d19 --- /dev/null +++ b/internal/controller/s3/handler.go @@ -0,0 +1,83 @@ +package s3 + +import ( + "context" + "encoding/base64" + "github.com/minio/madmin-go/v3" + stackv1alpha1 "github.com/zncdata-labs/zncdata-stack-operator/api/v1alpha1" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func CreateS3Config(s3Bucket *stackv1alpha1.S3Bucket) *Config { + config := &Config{} + return config +} + +func CreateS3AdminClient(ctx context.Context, c client.Client, s3Connection *stackv1alpha1.S3Connection) (*madmin.AdminClient, error) { + config, err := GetAdminConfig(ctx, c, s3Connection) + if err != nil { + return nil, err + } + adminClient, err := NewAdminClient(config) + if err != nil { + return nil, err + } + return adminClient, nil +} + +func GetAdminConfig(ctx context.Context, c client.Client, s3Connection *stackv1alpha1.S3Connection) (*Config, error) { + config := &Config{} + if s3Connection.Spec.S3Credential.ExistingSecret != "" { + secret := &corev1.Secret{} + err := c.Get(ctx, client.ObjectKey{ + Namespace: s3Connection.Namespace, + Name: s3Connection.Spec.S3Credential.ExistingSecret, + }, secret) + if err != nil { + return nil, err + } + if endpoint, ok := secret.Data["endpoint"]; ok { + decodeString, err := base64.StdEncoding.DecodeString(string(endpoint)) + if err != nil { + return nil, err + } + config.Endpoint = string(decodeString) + } + if accessKey, ok := secret.Data["accessKey"]; ok { + decodeString, err := base64.StdEncoding.DecodeString(string(accessKey)) + if err != nil { + return nil, err + } + config.AccessKey = string(decodeString) + } + if secretKey, ok := secret.Data["secretKey"]; ok { + decodeString, err := base64.StdEncoding.DecodeString(string(secretKey)) + if err != nil { + return nil, err + } + config.SecretKey = string(decodeString) + } + if region, ok := secret.Data["region"]; ok { + decodeString, err := base64.StdEncoding.DecodeString(string(region)) + if err != nil { + return nil, err + } + config.Region = string(decodeString) + } + if ssl, ok := secret.Data["ssl"]; ok { + decodeString, err := base64.StdEncoding.DecodeString(string(ssl)) + if err != nil { + return nil, err + } + config.SSL = string(decodeString) == "true" + } + } else { + config.Endpoint = s3Connection.Spec.S3Credential.Endpoint + config.AccessKey = s3Connection.Spec.S3Credential.AccessKey + config.SecretKey = s3Connection.Spec.S3Credential.SecretKey + config.Region = s3Connection.Spec.S3Credential.Region + config.SSL = s3Connection.Spec.S3Credential.SSL + } + return config, nil +} diff --git a/internal/controller/s3/s3_init.go b/internal/controller/s3/s3_init.go new file mode 100644 index 0000000..305dedc --- /dev/null +++ b/internal/controller/s3/s3_init.go @@ -0,0 +1,186 @@ +package s3 + +import ( + "context" + + "fmt" + "github.com/minio/madmin-go/v3" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "k8s.io/apimachinery/pkg/util/json" +) + +type Policy struct { + Version string `json:"Version,omitempty"` + Statements []*Statement `json:"Statement,omitempty"` +} +type Statement struct { + Sid string `json:"Sid,omitempty"` + Effect string `json:"Effect,omitempty"` + Principal Principal `json:"Principal,omitempty"` + Resource string `json:"Resource,omitempty"` + Action []string `json:"Action,omitempty"` +} +type Principal struct { + AWS []string `json:"AWS,omitempty"` +} + +type Config struct { + // 访问地址,ip:port + Endpoint string `json:"endpoint,omitempty"` + AccessKey string `json:"access_key,omitempty"` + SecretKey string `json:"secret_key,omitempty"` + Region string `json:"region,omitempty"` + Token string `json:"token,omitempty"` + SSL bool `json:"ssl,omitempty"` +} + +// NewAdminClient 用来管理权限的客户端 +func NewAdminClient(s3Config *Config) (*madmin.AdminClient, error) { + // Initialize minio client object. + mdmClnt, err := madmin.New(s3Config.Endpoint, s3Config.AccessKey, s3Config.SecretKey, s3Config.SSL) + if err != nil { + return nil, err + } + return mdmClnt, nil +} + +// NewClient 用来操作s3的客户端(上传下载,处理通相关逻辑) +func NewClient(s3Config *Config) (*minio.Client, error) { + // Initialize minio client object. + minioClient, err := minio.New(s3Config.Endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(s3Config.AccessKey, s3Config.SecretKey, s3Config.Token), + Secure: s3Config.SSL, + }) + if err != nil { + return nil, err + } + return minioClient, nil +} + +type Initializer struct { + IS3Initializer +} + +func (i Initializer) createBucket(ctx context.Context, bucketName, region string) error { + panic("implement me") +} + +func (i Initializer) createUser(ctx context.Context, s3Config *Config) error { + panic("implement me") +} + +func (i Initializer) createUserPolicy(ctx context.Context, policyName, accessKey, bucketName string) error { + panic("implement me") +} + +func (i Initializer) bindPolicy(ctx context.Context, accessKey, policyName string) error { + panic("implement me") +} + +func (i Initializer) removeUser(ctx context.Context, accessKey string) error { + panic("implement me") +} + +func (i Initializer) removeBucket(ctx context.Context, bucketName string) error { + panic("implement me") +} + +type MinioInitializer struct { + Initializer + adminClient *madmin.AdminClient + client *minio.Client +} + +func (mi *MinioInitializer) createUser(ctx context.Context, s3Config *Config) error { + return mi.adminClient.AddUser(ctx, s3Config.AccessKey, s3Config.SecretKey) +} + +func (mi *MinioInitializer) createBucket(ctx context.Context, bucketName, region string) error { + exists, err := mi.client.BucketExists(ctx, bucketName) + if err != nil { + return err + } + if exists { + return nil + } + return mi.client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{Region: region}) +} + +// generatePolicy - generate policy for user +func generatePolicy(accessKey, bucket string) []byte { + principalAws := fmt.Sprintf("arn:aws:iam::%s:root", accessKey) + resource := fmt.Sprintf("arn:aws:s3:::%s/*", bucket) + policy := &Policy{ + Version: "2012-10-17", + Statements: []*Statement{ + { + Sid: "ObjectLevel", + Effect: "Allow", + Principal: Principal{ + AWS: []string{principalAws}, + }, + Resource: resource, + Action: []string{"s3:*"}, + }, + { + Sid: "BucketLevel", + Effect: "Allow", + Principal: Principal{ + AWS: []string{principalAws}, + }, + Resource: resource, + Action: []string{"s3:*"}, + }, + }, + } + marshal, err := json.Marshal(policy) + if err != nil { + return nil + } + return marshal +} + +func (mi *MinioInitializer) createUserPolicy(ctx context.Context, policyName, accessKey, bucketName string) error { + policy := generatePolicy(accessKey, bucketName) + return mi.adminClient.AddCannedPolicy(ctx, policyName, policy) + +} + +func (mi *MinioInitializer) bindPolicy(ctx context.Context, accessKeyID, policyName string) error { + return mi.adminClient.SetPolicy(ctx, policyName, accessKeyID, false) +} + +func (mi *MinioInitializer) removeUser(ctx context.Context, accessKey string) error { + return mi.adminClient.RemoveUser(ctx, accessKey) +} + +func (mi *MinioInitializer) removeBucket(ctx context.Context, bucketName string) error { + return mi.client.RemoveBucket(ctx, bucketName) +} + +func NewMinioInitializer(s3Config *Config) (*MinioInitializer, error) { + admin, err := NewAdminClient(s3Config) + if err != nil { + return nil, err + } + + s3Client, err := NewClient(s3Config) + if err != nil { + return nil, err + } + + return &MinioInitializer{ + adminClient: admin, + client: s3Client, + }, nil +} + +type IS3Initializer interface { + createBucket(ctx context.Context, bucketName, region string) error + createUser(ctx context.Context, s3Config *Config) error + removeUser(ctx context.Context, accessKey string) error + removeBucket(ctx context.Context, bucketName string) error + createUserPolicy(ctx context.Context, policyName, accessKey, bucketName string) error + bindPolicy(ctx context.Context, accessKey, policyName string) error +} diff --git a/internal/controller/s3/s3_init_test.go b/internal/controller/s3/s3_init_test.go new file mode 100644 index 0000000..b0185d0 --- /dev/null +++ b/internal/controller/s3/s3_init_test.go @@ -0,0 +1,177 @@ +package s3 + +import ( + "context" + "fmt" + "github.com/zncdata-labs/zncdata-stack-operator/internal/util" + "os" + "strings" + "testing" +) + +// NewDefaultConfig minio 官网提供的配置 +func NewDefaultConfig() *Config { + getenv := os.Getenv("ENV") + if getenv == "local" { + return NewLocalConfig() + } + return NewConfigFromEnv() +} + +func NewLocalConfig() *Config { + return &Config{ + Endpoint: "127.0.0.1:9000", + AccessKey: "admin", + SecretKey: "admin123456", + Region: "us-east-1", + Token: "", + SSL: false, + } +} + +func NewConfigFromEnv() *Config { + endpoint := os.Getenv("MINIO_ENDPOINT") + accessKey := os.Getenv("MINIO_ACCESS_KEY") + secretKey := os.Getenv("MINIO_SECRET_KEY") + region := os.Getenv("MINIO_REGION") + Token := os.Getenv("MINIO_TOKEN") + ssl := os.Getenv("MINIO_SSL") + return &Config{ + Endpoint: endpoint, + AccessKey: accessKey, + SecretKey: secretKey, + Region: region, + Token: Token, + SSL: ssl == "true", + } +} + +func TestNewClient(t *testing.T) { + config := NewDefaultConfig() + client, err := NewClient(config) + ctx := context.Background() + if err != nil { + t.Error(err) + } + buckets, err := client.ListBuckets(ctx) + if err != nil { + t.Error(err) + } + t.Log("list buckets", "buckets: ", buckets) +} + +func TestListUser(t *testing.T) { + config := NewDefaultConfig() + client, err := NewAdminClient(config) + if err != nil { + t.Error("new admin", "err: ", err) + } + t.Log("new admin", "admin: ", client) + + users, err := client.ListUsers(context.Background()) + if err != nil { + t.Error("list users", "err: ", err) + } + t.Log("list users", "users: ", users) +} + +func TestS3Suit(t *testing.T) { + config := NewDefaultConfig() + ctx := context.Background() + initializer, err := NewMinioInitializer(config) + if err != nil { + t.Error("new minio initializer", "err: ", err) + } + + str := fmt.Sprintf("zncdata_minio_%s", util.GenerateRandomStr(5)) + accessKey := fmt.Sprintf("%s_user", str) + secretKey, err := util.GenerateSecretAccessKey() + if err != nil { + secretKey = util.GenerateRandomStr(10) + } + policyName := fmt.Sprintf("%s_policy", str) + bucketName := strings.ToLower(util.RemoveSpecialCharacter(fmt.Sprintf("%s_bucket", str))) + + newUserConfig := &Config{ + Endpoint: config.Endpoint, + AccessKey: accessKey, + SecretKey: secretKey, + Region: config.Region, + Token: "", + SSL: config.SSL, + } + err = initializer.createUser(ctx, newUserConfig) + if err != nil { + t.Error("create user", "err: ", err) + } + t.Log("create user", "accessKey: ", accessKey, "secretKey: ", secretKey) + + err = initializer.createBucket(ctx, bucketName, newUserConfig.Region) + if err != nil { + t.Error("create bucket", "err: ", err) + } + t.Log("create bucket", "bucketName: ", bucketName) + + err = initializer.createUserPolicy(ctx, policyName, accessKey, bucketName) + if err != nil { + t.Error("create user policy", "err: ", err) + } + t.Log("create user policy", "policyName: ", policyName, "accessKey: ", accessKey, "bucketName: ", bucketName) + + err = initializer.bindPolicy(ctx, accessKey, policyName) + if err != nil { + t.Error("bind policy", "err: ", err) + } + t.Log("bind policy", "policyName: ", policyName, "accessKey: ", accessKey) + + client, err := NewClient(newUserConfig) + if err != nil { + t.Error("new client", "err: ", err) + } + t.Log("new client", "client: ", client) + + exists, err := client.BucketExists(ctx, bucketName) + if err != nil { + t.Error("bucket exists", "err: ", err) + } + t.Log("bucket exists", "exists: ", exists) + + err = client.RemoveBucket(ctx, bucketName) + if err != nil { + t.Error("remove bucket", "err: ", err) + } + t.Log("remove bucket", "bucketName: ", bucketName) + + err = initializer.removeUser(ctx, accessKey) + if err != nil { + t.Error("remove user", "err: ", err) + } + t.Log("remove user", "accessKey: ", accessKey) +} + +func Test_generatePolicy(t *testing.T) { + type args struct { + accessKey string + bucket string + } + tests := []struct { + name string + args args + want []byte + }{ + { + name: "test generate policy 1", + args: args{ + accessKey: "test_000_zncdata_1", + bucket: "test_000_zncdata_1", + }, + want: []byte("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"ObjectLevel\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"arn:aws:iam::test_000_zncdata_1:root\"]},\"Resource\":\"arn:aws:s3:::test_000_zncdata_1/*\",\"Action\":[\"s3:*\"]},{\"Sid\":\"BucketLevel\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"arn:aws:iam::test_000_zncdata_1:root\"]},\"Resource\":\"arn:aws:s3:::test_000_zncdata_1/*\",\"Action\":[\"s3:*\"]}]}"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := generatePolicy(tt.args.accessKey, tt.args.bucket) + t.Log("generate policy", "got: ", string(got), "want: ", string(tt.want)) + }) + } +} diff --git a/internal/controller/s3/s3bucket_controller.go b/internal/controller/s3/s3bucket_controller.go new file mode 100644 index 0000000..24c95b2 --- /dev/null +++ b/internal/controller/s3/s3bucket_controller.go @@ -0,0 +1,296 @@ +/* +Copyright 2023 zncdata-labs. + +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 s3 + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "github.com/go-logr/logr" + "github.com/zncdata-labs/zncdata-stack-operator/internal/util" + corev1 "k8s.io/api/core/v1" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + stackv1alpha1 "github.com/zncdata-labs/zncdata-stack-operator/api/v1alpha1" +) + +// S3BucketReconciler reconciles a S3Bucket object +type S3BucketReconciler struct { + client.Client + Scheme *runtime.Scheme + Log logr.Logger +} + +//+kubebuilder:rbac:groups=stack.zncdata.net,resources=s3buckets,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=stack.zncdata.net,resources=s3buckets/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=stack.zncdata.net,resources=s3buckets/finalizers,verbs=update +// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the S3Bucket object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.1/pkg/reconcile +func (r *S3BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + s3Bucket := &stackv1alpha1.S3Bucket{} + if err := r.Get(ctx, req.NamespacedName, s3Bucket); err != nil { + if client.IgnoreNotFound(err) != nil { + r.Log.Error(err, "unable to fetch instance") + return ctrl.Result{}, err + } + r.Log.Info("s3Bucket resource not found. Ignoring since object must be deleted") + return ctrl.Result{}, nil + } + //// Get the status condition, if it exists and its generation is not the + ////same as the s3Bucket's generation, reset the status conditions + readCondition := apimeta.FindStatusCondition(s3Bucket.Status.Conditions, stackv1alpha1.ConditionTypeProgressing) + if readCondition == nil || readCondition.ObservedGeneration != s3Bucket.GetGeneration() { + s3Bucket.InitStatusConditions() + if err := r.UpdateStatus(ctx, s3Bucket); err != nil { + return ctrl.Result{}, err + } + } + + if err := r.CreateBucket(ctx, s3Bucket); err != nil { + r.Log.Error(err, "create bucket") + return ctrl.Result{}, err + } + r.Log.Info("create bucket success") + + isDatabaseMarkedToBeDeleted := s3Bucket.GetDeletionTimestamp() != nil + if isDatabaseMarkedToBeDeleted { + if controllerutil.ContainsFinalizer(s3Bucket, stackv1alpha1.S3BucketFinalizer) { + if err := r.finalizeDatabase(ctx, s3Bucket); err != nil { + return ctrl.Result{}, err + } + controllerutil.RemoveFinalizer(s3Bucket, stackv1alpha1.S3BucketFinalizer) + if err := r.Update(ctx, s3Bucket); err != nil { + return ctrl.Result{}, err + } + } + return ctrl.Result{}, nil + } + + // Add finalizer for this CR + if !controllerutil.ContainsFinalizer(s3Bucket, stackv1alpha1.S3BucketFinalizer) { + controllerutil.AddFinalizer(s3Bucket, stackv1alpha1.S3BucketFinalizer) + err := r.Update(ctx, s3Bucket) + if err != nil { + return ctrl.Result{}, err + } + } + + if s3Bucket.IsAvailable() { + return ctrl.Result{}, nil + } + + s3Bucket.SetStatusCondition(metav1.Condition{ + Type: stackv1alpha1.ConditionTypeAvailable, + Status: metav1.ConditionTrue, + Reason: stackv1alpha1.ConditionReasonRunning, + Message: "DatabaseConnection is running", + ObservedGeneration: s3Bucket.GetGeneration(), + }) + + if err := r.UpdateStatus(ctx, s3Bucket); err != nil { + r.Log.Error(err, "Failed to update status") + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *S3BucketReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&stackv1alpha1.S3Bucket{}). + Complete(r) +} + +func (r *S3BucketReconciler) UpdateStatus(ctx context.Context, instance *stackv1alpha1.S3Bucket) error { + retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { + return r.Status().Update(ctx, instance) + }) + if retryErr != nil { + r.Log.Error(retryErr, "Failed to update vfm status after retries") + return retryErr + } + r.Log.V(1).Info("Successfully patched object status") + return nil +} + +func (r *S3BucketReconciler) CreateBucket(ctx context.Context, s3bucket *stackv1alpha1.S3Bucket) error { + // 如果可用,或者要删除了,就不再创建 + if s3bucket.IsAvailable() || s3bucket.GetDeletionTimestamp() != nil || controllerutil.ContainsFinalizer(s3bucket, stackv1alpha1.S3BucketFinalizer) { + return nil + } + if s3bucket.Spec.Reference != "" { + if s3bucket.Spec.Name == "" { + return errors.New("bucket name is empty") + } + s3Connection := &stackv1alpha1.S3Connection{} + err := r.Get(ctx, client.ObjectKey{Namespace: s3bucket.Namespace, Name: s3bucket.Spec.Reference}, s3Connection) + if err != nil { + return err + } + adminConfig, err := GetAdminConfig(ctx, r.Client, s3Connection) + if err != nil { + return err + } + initializer, err := NewMinioInitializer(adminConfig) + if err != nil { + return err + } + + accessKey := fmt.Sprintf("%s%s", s3bucket.Name, util.GenerateRandomStr(5)) + secretKey := util.GenerateRandomStr(10) + bucketName := s3bucket.Spec.Name + policyName := fmt.Sprintf("zncdata_%s_%s", accessKey, bucketName) + newUserConfig := &Config{ + Endpoint: adminConfig.Endpoint, + AccessKey: accessKey, + SecretKey: secretKey, + Region: adminConfig.Region, + SSL: adminConfig.SSL, + } + + err = initializer.createBucket(ctx, s3bucket.Spec.Name, adminConfig.Region) + if err != nil { + return err + } + err = initializer.createUser(ctx, newUserConfig) + if err != nil { + return err + } + err = initializer.createUserPolicy(ctx, policyName, accessKey, bucketName) + if err != nil { + return err + } + err = initializer.bindPolicy(ctx, accessKey, policyName) + if err != nil { + return err + } + sData := make(map[string][]byte) + sData["accessKey"] = []byte(accessKey) + sData["secretKey"] = []byte(secretKey) + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-secret", s3bucket.Name), + Namespace: s3Connection.Namespace, + }, + Type: corev1.SecretTypeOpaque, + Data: sData, + } + err = util.CreateOrUpdate(ctx, r.Client, secret) + if err != nil { + return err + } + s3bucket.Spec.Credential = &stackv1alpha1.S3BucketCredential{ + ExistingSecret: secretKey, + } + err = r.Update(ctx, s3bucket) + if err != nil { + return err + } + } else { + return errors.New("no reference found") + } + + return nil +} + +func (r *S3BucketReconciler) finalizeDatabase(ctx context.Context, s3bucket *stackv1alpha1.S3Bucket) error { + if s3bucket.Spec.Credential != nil && s3bucket.Spec.Credential.ExistingSecret != "" { + s3Connection := &stackv1alpha1.S3Connection{} + err := r.Get(ctx, client.ObjectKey{Namespace: s3bucket.Namespace, Name: s3bucket.Spec.Credential.ExistingSecret}, s3Connection) + if err != nil { + return err + } + adminConfig, err := GetAdminConfig(ctx, r.Client, s3Connection) + if err != nil { + return err + } + initializer, err := NewMinioInitializer(adminConfig) + if err != nil { + return err + } + config := &Config{ + Endpoint: adminConfig.Endpoint, + Region: adminConfig.Region, + SSL: adminConfig.SSL, + Token: adminConfig.Token, + } + if s3bucket.Spec.Credential.ExistingSecret != "" { + secret := corev1.Secret{} + err := r.Get(ctx, client.ObjectKey{Namespace: s3bucket.Namespace, Name: s3bucket.Spec.Credential.ExistingSecret}, &secret) + if err != nil { + return err + } + if accessKey, ok := secret.Data["accessKey"]; ok { + decodeString, err := base64.StdEncoding.DecodeString(string(accessKey)) + if err != nil { + return err + } + config.AccessKey = string(decodeString) + } + if secretKey, ok := secret.Data["secretKey"]; ok { + decodeString, err := base64.StdEncoding.DecodeString(string(secretKey)) + if err != nil { + return err + } + config.SecretKey = string(decodeString) + } + + } else { + return errors.New("no secret found") + } + + if s3bucket.Spec.Name == "" { + return errors.New("bucket name is empty") + } + err = initializer.removeUser(ctx, config.AccessKey) + if err != nil { + return err + } + err = initializer.removeBucket(ctx, s3bucket.Spec.Name) + if err != nil { + return err + } + + } else { + return errors.New("no reference found") + } + + return nil + +} diff --git a/internal/controller/s3/s3connection_controller.go b/internal/controller/s3/s3connection_controller.go new file mode 100644 index 0000000..77213a7 --- /dev/null +++ b/internal/controller/s3/s3connection_controller.go @@ -0,0 +1,124 @@ +/* +Copyright 2023 zncdata-labs. + +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 s3 + +import ( + "context" + "github.com/go-logr/logr" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/retry" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + stackv1alpha1 "github.com/zncdata-labs/zncdata-stack-operator/api/v1alpha1" +) + +// S3ConnectionReconciler reconciles a S3Connection object +type S3ConnectionReconciler struct { + client.Client + Scheme *runtime.Scheme + Log logr.Logger +} + +//+kubebuilder:rbac:groups=stack.zncdata.net,resources=s3connections,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=stack.zncdata.net,resources=s3connections/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=stack.zncdata.net,resources=s3connections/finalizers,verbs=update +// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the S3Connection object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.1/pkg/reconcile +func (r *S3ConnectionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + s3Connection := &stackv1alpha1.S3Connection{} + if err := r.Get(ctx, req.NamespacedName, s3Connection); err != nil { + if client.IgnoreNotFound(err) != nil { + r.Log.Error(err, "unable to fetch instance") + return ctrl.Result{}, err + } + r.Log.Info("s3Connection resource not found. Ignoring since object must be deleted") + return ctrl.Result{}, nil + } + //// Get the status condition, if it exists and its generation is not the + ////same as the s3Connection's generation, reset the status conditions + readCondition := apimeta.FindStatusCondition(s3Connection.Status.Conditions, stackv1alpha1.ConditionTypeProgressing) + if readCondition == nil || readCondition.ObservedGeneration != s3Connection.GetGeneration() { + s3Connection.InitStatusConditions() + if err := r.UpdateStatus(ctx, s3Connection); err != nil { + return ctrl.Result{}, err + } + } + + _, err := CreateS3AdminClient(ctx, r.Client, s3Connection) + if err != nil { + r.Log.Error(err, "create s3 admin client", "err: ", err) + return ctrl.Result{}, err + } + + r.Log.Info("s3Connection is found", "Name", s3Connection.Name) + + if s3Connection.IsAvailable() { + return ctrl.Result{}, err + } + + s3Connection.SetStatusCondition(metav1.Condition{ + Type: stackv1alpha1.ConditionTypeAvailable, + Status: metav1.ConditionTrue, + Reason: stackv1alpha1.ConditionReasonRunning, + Message: "s3Connection is running", + ObservedGeneration: s3Connection.GetGeneration(), + }) + + if err := r.UpdateStatus(ctx, s3Connection); err != nil { + r.Log.Error(err, "Failed to update status") + return ctrl.Result{}, err + } + + r.Log.Info("Successfully updated status") + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *S3ConnectionReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&stackv1alpha1.S3Connection{}). + Complete(r) +} + +func (r *S3ConnectionReconciler) UpdateStatus(ctx context.Context, instance *stackv1alpha1.S3Connection) error { + retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { + return r.Status().Update(ctx, instance) + }) + if retryErr != nil { + r.Log.Error(retryErr, "Failed to update vfm status after retries") + return retryErr + } + r.Log.V(1).Info("Successfully patched object status") + return nil +} diff --git a/internal/util/create_utils.go b/internal/util/create_utils.go new file mode 100644 index 0000000..8129564 --- /dev/null +++ b/internal/util/create_utils.go @@ -0,0 +1,89 @@ +package util + +import ( + "context" + "fmt" + ctrl "sigs.k8s.io/controller-runtime" + + "github.com/cisco-open/k8s-objectmatcher/patch" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubectl/pkg/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + logger = ctrl.Log.WithName("utils") +) + +func CreateOrUpdate(ctx context.Context, c client.Client, obj client.Object) error { + key := client.ObjectKeyFromObject(obj) + namespace := obj.GetNamespace() + + kinds, _, _ := scheme.Scheme.ObjectKinds(obj) + + name := obj.GetName() + current := obj.DeepCopyObject().(client.Object) + // Check if the object exists, if not create a new one + err := c.Get(ctx, key, current) + if errors.IsNotFound(err) { + if err := patch.DefaultAnnotator.SetLastAppliedAnnotation(obj); err != nil { + return err + } + logger.Info("Creating a new object", "Kind", kinds, "Namespace", namespace, "Name", name) + return c.Create(ctx, obj) + } else if err == nil { + switch obj.(type) { + case *corev1.Service: + currentSvc := current.(*corev1.Service) + svc := obj.(*corev1.Service) + // Preserve the ClusterIP when updating the service + svc.Spec.ClusterIP = currentSvc.Spec.ClusterIP + // Preserve the annotation when updating the service, ensure any updated annotation is preserved + //for key, value := range currentSvc.Annotations { + // if _, present := svc.Annotations[key]; !present { + // svc.Annotations[key] = value + // } + //} + + if svc.Spec.Type == corev1.ServiceTypeNodePort || svc.Spec.Type == corev1.ServiceTypeLoadBalancer { + for i := range svc.Spec.Ports { + svc.Spec.Ports[i].NodePort = currentSvc.Spec.Ports[i].NodePort + } + } + } + result, err := patch.DefaultPatchMaker.Calculate(current, obj, patch.IgnoreStatusFields()) + if err != nil { + logger.Error(err, "failed to calculate patch to match objects, moving on to update") + // if there is an error with matching, we still want to update + resourceVersion := current.(metav1.ObjectMetaAccessor).GetObjectMeta().GetResourceVersion() + obj.(metav1.ObjectMetaAccessor).GetObjectMeta().SetResourceVersion(resourceVersion) + + return c.Update(ctx, obj) + } + + if !result.IsEmpty() { + logger.Info(fmt.Sprintf("Resource update for object %s:%s", kinds, obj.(metav1.ObjectMetaAccessor).GetObjectMeta().GetName()), + "patch", string(result.Patch), + // "original", string(result.Original), + // "modified", string(result.Modified), + // "current", string(result.Current), + ) + + err := patch.DefaultAnnotator.SetLastAppliedAnnotation(obj) + if err != nil { + logger.Error(err, "failed to annotate modified object", "object", obj) + } + + resourceVersion := current.(metav1.ObjectMetaAccessor).GetObjectMeta().GetResourceVersion() + obj.(metav1.ObjectMetaAccessor).GetObjectMeta().SetResourceVersion(resourceVersion) + + return c.Update(ctx, obj) + } + + logger.V(1).Info(fmt.Sprintf("Skipping update for object %s:%s", kinds, obj.(metav1.ObjectMetaAccessor).GetObjectMeta().GetName())) + + } + return err +} diff --git a/internal/util/random.go b/internal/util/random.go new file mode 100644 index 0000000..d637066 --- /dev/null +++ b/internal/util/random.go @@ -0,0 +1,47 @@ +package util + +import ( + "crypto/rand" + "encoding/base64" + "errors" + mathRand "math/rand" + "regexp" +) + +const ( + minioSecretIDLength = 40 +) + +// GenerateSecretAccessKey - generate random base64 numeric value from a random seed. +func GenerateSecretAccessKey() (string, error) { + rb := make([]byte, minioSecretIDLength) + if _, e := rand.Read(rb); e != nil { + return "", errors.New("could not generate Secret Key") + } + + return string(Encode(rb)), nil +} + +// Encode queues message +func Encode(value []byte) []byte { + length := len(value) + encoded := make([]byte, base64.URLEncoding.EncodedLen(length)) + base64.URLEncoding.Encode(encoded, value) + return encoded +} + +func GenerateRandomStr(length int) string { + letterBytes := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + bytes := make([]byte, length) + for i := range bytes { + bytes[i] = letterBytes[mathRand.Intn(len(letterBytes))] + } + + return string(bytes) +} + +func RemoveSpecialCharacter(str string) string { + regex := regexp.MustCompile("[^a-zA-Z0-9]+") + result := regex.ReplaceAllString(str, "") + return result +} diff --git a/internal/util/random_test.go b/internal/util/random_test.go new file mode 100644 index 0000000..d73be06 --- /dev/null +++ b/internal/util/random_test.go @@ -0,0 +1,8 @@ +package util + +import "testing" + +func Test_generateRandomStr(t *testing.T) { + str := GenerateRandomStr(5) + t.Log(str) +}