diff --git a/core/clients/server.go b/core/clients/server.go index 82f44ea..605af83 100644 --- a/core/clients/server.go +++ b/core/clients/server.go @@ -40,13 +40,13 @@ type ServerV1 interface { SecretUpdate(ctx context.Context, data *v1.Secret) error SecretDelete(ctx context.Context, namespace, name string) error - WorkflowList(ctx context.Context, namespace string, page v1.Pagination) ([]*v1.Workflow, *v1.Pagination, error) + WorkflowList(ctx context.Context, namespace string, opt v1.ListOption) ([]*v1.Workflow, *v1.Pagination, error) WorkflowInfo(ctx context.Context, namespace, name string) (*v1.Workflow, error) WorkflowCreate(ctx context.Context, data *v1.Workflow) error WorkflowUpdate(ctx context.Context, data *v1.Workflow) error WorkflowDelete(ctx context.Context, namespace, name string) error - BoxList(ctx context.Context, namespace string, page v1.Pagination) ([]*v1.Box, *v1.Pagination, error) + BoxList(ctx context.Context, namespace string, opt v1.ListOption) ([]*v1.Box, *v1.Pagination, error) BoxInfo(ctx context.Context, namespace, name string) (*v1.Box, error) BoxCreate(ctx context.Context, data *v1.Box) error BoxUpdate(ctx context.Context, data *v1.Box) error @@ -134,16 +134,15 @@ func (c *serverV1) SecretDelete(ctx context.Context, namespace, name string) err return handleClientError(resp, err) } -func (c *serverV1) WorkflowList(ctx context.Context, namespace string, page v1.Pagination) ([]*v1.Workflow, *v1.Pagination, error) { +func (c *serverV1) WorkflowList(ctx context.Context, namespace string, opt v1.ListOption) ([]*v1.Workflow, *v1.Pagination, error) { type resultT struct { v1.Pagination Items []*v1.Workflow `json:"items"` } var result resultT - vs := page.ToValues() req := c.R(ctx). - SetQueryParamsFromValues(vs). + SetQueryParamsFromValues(opt.ToValues()). SetPathParam("namespace", namespace). SetResult(&result) resp, err := req.Get("/workflow/{namespace}") @@ -189,16 +188,15 @@ func (c *serverV1) WorkflowDelete(ctx context.Context, namespace, name string) e return handleClientError(resp, err) } -func (c *serverV1) BoxList(ctx context.Context, namespace string, page v1.Pagination) ([]*v1.Box, *v1.Pagination, error) { +func (c *serverV1) BoxList(ctx context.Context, namespace string, opt v1.ListOption) ([]*v1.Box, *v1.Pagination, error) { type resultT struct { v1.Pagination Items []*v1.Box `json:"items"` } var result resultT - vs := page.ToValues() req := c.R(ctx). - SetQueryParamsFromValues(vs). + SetQueryParamsFromValues(opt.ToValues()). SetPathParam("namespace", namespace). SetResult(&result) resp, err := req.Get("/box/{namespace}") diff --git a/core/command/ctl.go b/core/command/ctl.go index da955a0..c35c328 100644 --- a/core/command/ctl.go +++ b/core/command/ctl.go @@ -150,7 +150,10 @@ func workflowList(cmd *cobra.Command, _ []string) error { if err != nil { return err } - result, _, err := sc.WorkflowList(context.Background(), v1.AllNamespace, *getPage(cmd)) + opt := v1.ListOption{ + Pagination: *getPage(cmd), + } + result, _, err := sc.WorkflowList(context.Background(), v1.AllNamespace, opt) if err != nil { return err } @@ -209,7 +212,10 @@ func boxList(cmd *cobra.Command, _ []string) error { if err != nil { return err } - result, _, err := sc.BoxList(context.Background(), v1.AllNamespace, *getPage(cmd)) + opt := v1.ListOption{ + Pagination: *getPage(cmd), + } + result, _, err := sc.BoxList(context.Background(), v1.AllNamespace, opt) if err != nil { return err } diff --git a/core/handler/server/box.go b/core/handler/server/box.go index d5fcd20..c7ba806 100644 --- a/core/handler/server/box.go +++ b/core/handler/server/box.go @@ -29,8 +29,10 @@ func boxList(boxSrv service.Box) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { namespace := wrapper.URLParam(r, "namespace") page := v1.GetPagination(r) - - result, err := boxSrv.List(r.Context(), namespace, page) + result, err := boxSrv.List(r.Context(), namespace, v1.ListOption{ + Pagination: *page, + LabelSelector: r.URL.Query().Get("labelSelector"), + }) if err != nil { wrapper.InternalError(w, err) return diff --git a/core/handler/server/workflow.go b/core/handler/server/workflow.go index 55af146..9214759 100644 --- a/core/handler/server/workflow.go +++ b/core/handler/server/workflow.go @@ -29,7 +29,10 @@ func workflowList(workflowSrv service.Workflow) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { namespace := wrapper.URLParam(r, "namespace") page := v1.GetPagination(r) - result, err := workflowSrv.List(r.Context(), namespace, page) + result, err := workflowSrv.List(r.Context(), namespace, v1.ListOption{ + Pagination: *page, + LabelSelector: r.URL.Query().Get("labelSelector"), + }) if err != nil { wrapper.InternalError(w, err) return diff --git a/core/service/box/box.go b/core/service/box/box.go index b47ec9f..63f5854 100644 --- a/core/service/box/box.go +++ b/core/service/box/box.go @@ -19,6 +19,7 @@ import ( "github.com/zc2638/ink/core/constant" "github.com/zc2638/ink/core/service" + "github.com/zc2638/ink/core/service/common" v1 "github.com/zc2638/ink/pkg/api/core/v1" storageV1 "github.com/zc2638/ink/pkg/api/storage/v1" "github.com/zc2638/ink/pkg/database" @@ -30,21 +31,28 @@ func New() service.Box { type srv struct{} -func (s *srv) List(ctx context.Context, namespace string, page *v1.Pagination) ([]*v1.Box, error) { +func (s *srv) List(ctx context.Context, namespace string, opt v1.ListOption) ([]*v1.Box, error) { db := database.FromContext(ctx) - var ( - list []storageV1.Box - total int64 - ) + labels := opt.Labels() + if len(labels) > 0 { + names, err := common.SelectNamesByLabels(ctx, v1.KindBox, namespace, labels) + if err != nil { + return nil, err + } + if len(names) == 0 { + return nil, nil + } + db = db.Where("name in (?)", names) + } if len(namespace) > 0 { db = db.Where("namespace = ?", namespace) } - if err := db.Model(&storageV1.Box{}).Count(&total).Error; err != nil { + if err := db.Model(&storageV1.Box{}).Count(&opt.Pagination.Total).Error; err != nil { return nil, err } - page.SetTotal(total) - if err := db.Scopes(page.Scope).Find(&list).Error; err != nil { + var list []storageV1.Box + if err := db.Scopes(opt.Pagination.Scope).Find(&list).Error; err != nil { return nil, err } diff --git a/core/service/common/label.go b/core/service/common/label.go new file mode 100644 index 0000000..ed62c4c --- /dev/null +++ b/core/service/common/label.go @@ -0,0 +1,58 @@ +// Copyright © 2023 zc2638 . +// +// 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 common + +import ( + "context" + "fmt" + + "github.com/99nil/gopkg/sets" + + storageV1 "github.com/zc2638/ink/pkg/api/storage/v1" + "github.com/zc2638/ink/pkg/database" +) + +func SelectNamesByLabels(ctx context.Context, kind, namespace string, labels map[string]string) ([]string, error) { + if len(labels) == 0 { + return nil, nil + } + + db := database.FromContext(ctx) + db = db.Where(&storageV1.Label{Namespace: namespace, Kind: kind}) + + var start bool + nameSet := sets.New[string]() + for k, v := range labels { + var selectNames []string + if err := db.Where("key = ?", k).Where("value = ?", v).Pluck("name", &selectNames).Error; err != nil { + return nil, fmt.Errorf("select label(%s=%s) failed: %v", k, v, err) + } + + if !start { + start = true + nameSet.Add(selectNames...) + continue + } + for _, sn := range selectNames { + if !nameSet.Has(sn) { + nameSet.Remove(sn) + } + } + if nameSet.Len() == 0 { + return nil, nil + } + } + return nameSet.List(), nil +} diff --git a/core/service/service.go b/core/service/service.go index 2ffee89..7c6b055 100644 --- a/core/service/service.go +++ b/core/service/service.go @@ -22,7 +22,7 @@ import ( type ( Workflow interface { - List(ctx context.Context, namespace string, page *v1.Pagination) ([]*v1.Workflow, error) + List(ctx context.Context, namespace string, opt v1.ListOption) ([]*v1.Workflow, error) Info(ctx context.Context, namespace, name string) (*v1.Workflow, error) Create(ctx context.Context, data *v1.Workflow) error Update(ctx context.Context, data *v1.Workflow) error @@ -30,7 +30,7 @@ type ( } Box interface { - List(ctx context.Context, namespace string, page *v1.Pagination) ([]*v1.Box, error) + List(ctx context.Context, namespace string, opt v1.ListOption) ([]*v1.Box, error) Info(ctx context.Context, namespace, name string) (*v1.Box, error) Create(ctx context.Context, data *v1.Box) error Update(ctx context.Context, data *v1.Box) error diff --git a/core/service/workflow/workflow.go b/core/service/workflow/workflow.go index 779637c..026296e 100644 --- a/core/service/workflow/workflow.go +++ b/core/service/workflow/workflow.go @@ -19,6 +19,7 @@ import ( "github.com/zc2638/ink/core/constant" "github.com/zc2638/ink/core/service" + "github.com/zc2638/ink/core/service/common" v1 "github.com/zc2638/ink/pkg/api/core/v1" storageV1 "github.com/zc2638/ink/pkg/api/storage/v1" "github.com/zc2638/ink/pkg/database" @@ -30,22 +31,29 @@ func New() service.Workflow { type srv struct{} -func (s *srv) List(ctx context.Context, namespace string, page *v1.Pagination) ([]*v1.Workflow, error) { +func (s *srv) List(ctx context.Context, namespace string, opt v1.ListOption) ([]*v1.Workflow, error) { db := database.FromContext(ctx) - var ( - list []storageV1.Workflow - total int64 - ) + labels := opt.Labels() + if len(labels) > 0 { + names, err := common.SelectNamesByLabels(ctx, v1.KindWorkflow, namespace, labels) + if err != nil { + return nil, err + } + if len(names) == 0 { + return nil, nil + } + db = db.Where("name in (?)", names) + } if len(namespace) > 0 { db = db.Where("namespace = ?", namespace) } - if err := db.Model(&storageV1.Workflow{}).Count(&total).Error; err != nil { + if err := db.Model(&storageV1.Workflow{}).Count(&opt.Pagination.Total).Error; err != nil { return nil, err } - page.SetTotal(total) - if err := db.Scopes(page.Scope).Find(&list).Error; err != nil { + var list []storageV1.Workflow + if err := db.Scopes(opt.Pagination.Scope).Find(&list).Error; err != nil { return nil, err } diff --git a/pkg/api/core/v1/option.go b/pkg/api/core/v1/option.go new file mode 100644 index 0000000..12e0d63 --- /dev/null +++ b/pkg/api/core/v1/option.go @@ -0,0 +1,67 @@ +// Copyright © 2023 zc2638 . +// +// 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 v1 + +import ( + "net/url" + "strings" +) + +type ListOption struct { + Pagination Pagination + LabelSelector string +} + +func (o *ListOption) ToValues() url.Values { + result := url.Values{} + for k, v := range o.Pagination.ToValues() { + result[k] = v + } + if len(o.LabelSelector) > 0 { + result.Set("labelSelector", o.LabelSelector) + } + return result +} + +func (o *ListOption) SetLabels(labels map[string]string) { + var sb strings.Builder + for k, v := range labels { + sb.WriteString(k) + sb.WriteString("=") + sb.WriteString(v) + sb.WriteString(",") + } + s := sb.String() + s = strings.TrimSuffix(s, ",") + o.LabelSelector = s +} + +func (o *ListOption) Labels() map[string]string { + selector := strings.TrimSpace(o.LabelSelector) + if len(selector) == 0 { + return nil + } + + labels := make(map[string]string) + parts := strings.Split(o.LabelSelector, ",") + for _, part := range parts { + kv := strings.Split(part, "=") + if len(kv) != 2 { + continue + } + labels[kv[0]] = kv[1] + } + return labels +} diff --git a/pkg/api/core/v1/workflow.go b/pkg/api/core/v1/workflow.go index 4dee01c..9c77ab3 100644 --- a/pkg/api/core/v1/workflow.go +++ b/pkg/api/core/v1/workflow.go @@ -32,7 +32,7 @@ func (w *Workflow) Worker() *Worker { type WorkflowSpec struct { Steps []Flow `json:"steps" yaml:"steps"` WorkingDir string `json:"workingDir,omitempty" yaml:"workingDir,omitempty"` - Concurrency int `json:"concurrency,omitempty" yaml:"concurrency,omitempty"` + Concurrency int `json:"concurrency,omitempty" yaml:"concurrency,5omitempty"` Volumes []Volume `json:"volumes,omitempty" yaml:"volumes,omitempty"` DependsOn []string `json:"dependsOn,omitempty" yaml:"dependsOn,omitempty"` ImagePullSecrets []string `json:"imagePullSecrets,omitempty" yaml:"imagePullSecrets,omitempty"` diff --git a/pkg/api/storage/v1/storage.go b/pkg/api/storage/v1/storage.go index 2ceb3e6..8d847a3 100644 --- a/pkg/api/storage/v1/storage.go +++ b/pkg/api/storage/v1/storage.go @@ -35,6 +35,21 @@ func (m *Model) GetID() uint64 { return m.ID } +type Label struct { + ID int + Namespace string + Name string + Kind string + Key string + Value string + + CreatedAt time.Time +} + +func (Label) TableName() string { + return "labels" +} + type Secret struct { Model diff --git a/resource/database/migrations/mysql/000008_create_labels_table.down.sql b/resource/database/migrations/mysql/000008_create_labels_table.down.sql new file mode 100644 index 0000000..676f4b8 --- /dev/null +++ b/resource/database/migrations/mysql/000008_create_labels_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `labels`; \ No newline at end of file diff --git a/resource/database/migrations/mysql/000008_create_labels_table.up.sql b/resource/database/migrations/mysql/000008_create_labels_table.up.sql new file mode 100644 index 0000000..752cf71 --- /dev/null +++ b/resource/database/migrations/mysql/000008_create_labels_table.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS `labels` +( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `namespace` VARCHAR(255) NOT NULL, + `name` VARCHAR(255) NOT NULL, + `kind` VARCHAR(255) NOT NULL, + `key` VARCHAR(255) NOT NULL, + `value` VARCHAR(255), + + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/resource/database/migrations/sqlite/000008_create_labels_table.down.sql b/resource/database/migrations/sqlite/000008_create_labels_table.down.sql new file mode 100644 index 0000000..676f4b8 --- /dev/null +++ b/resource/database/migrations/sqlite/000008_create_labels_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `labels`; \ No newline at end of file diff --git a/resource/database/migrations/sqlite/000008_create_labels_table.up.sql b/resource/database/migrations/sqlite/000008_create_labels_table.up.sql new file mode 100644 index 0000000..b8781ea --- /dev/null +++ b/resource/database/migrations/sqlite/000008_create_labels_table.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS `labels` +( + `id` INTEGER AUTO_INCREMENT, + `namespace` VARCHAR(255) NOT NULL, + `name` VARCHAR(255) NOT NULL, + `kind` VARCHAR(255) NOT NULL, + `key` VARCHAR(255) NOT NULL, + `value` VARCHAR(255), + + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; \ No newline at end of file