From 4bc02ec956987729fad68c93d84bacd2cc946564 Mon Sep 17 00:00:00 2001 From: Vic Xu Date: Wed, 19 Nov 2025 20:12:59 +0800 Subject: [PATCH 01/18] init project layout --- decisionmaker/client/.keep | 0 decisionmaker/cmd/.keep | 0 decisionmaker/domain/.keep | 0 decisionmaker/rest/.keep | 0 decisionmaker/service/.keep | 0 manager/client/.keep | 0 manager/cmd/.keep | 0 manager/domain/.keep | 0 manager/repository/.keep | 0 manager/rest/.keep | 0 manager/service/.keep | 0 11 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 decisionmaker/client/.keep create mode 100644 decisionmaker/cmd/.keep create mode 100644 decisionmaker/domain/.keep create mode 100644 decisionmaker/rest/.keep create mode 100644 decisionmaker/service/.keep create mode 100644 manager/client/.keep create mode 100644 manager/cmd/.keep create mode 100644 manager/domain/.keep create mode 100644 manager/repository/.keep create mode 100644 manager/rest/.keep create mode 100644 manager/service/.keep diff --git a/decisionmaker/client/.keep b/decisionmaker/client/.keep new file mode 100644 index 0000000..e69de29 diff --git a/decisionmaker/cmd/.keep b/decisionmaker/cmd/.keep new file mode 100644 index 0000000..e69de29 diff --git a/decisionmaker/domain/.keep b/decisionmaker/domain/.keep new file mode 100644 index 0000000..e69de29 diff --git a/decisionmaker/rest/.keep b/decisionmaker/rest/.keep new file mode 100644 index 0000000..e69de29 diff --git a/decisionmaker/service/.keep b/decisionmaker/service/.keep new file mode 100644 index 0000000..e69de29 diff --git a/manager/client/.keep b/manager/client/.keep new file mode 100644 index 0000000..e69de29 diff --git a/manager/cmd/.keep b/manager/cmd/.keep new file mode 100644 index 0000000..e69de29 diff --git a/manager/domain/.keep b/manager/domain/.keep new file mode 100644 index 0000000..e69de29 diff --git a/manager/repository/.keep b/manager/repository/.keep new file mode 100644 index 0000000..e69de29 diff --git a/manager/rest/.keep b/manager/rest/.keep new file mode 100644 index 0000000..e69de29 diff --git a/manager/service/.keep b/manager/service/.keep new file mode 100644 index 0000000..e69de29 From 8b7c414f4032bcfdf2244ebb8df56c11384f7d43 Mon Sep 17 00:00:00 2001 From: Vic Xu Date: Wed, 19 Nov 2025 20:51:28 +0800 Subject: [PATCH 02/18] feature/account: define interface and model --- go.mod | 1 + go.sum | 2 ++ manager/domain/account.go | 43 +++++++++++++++++++++++++ manager/domain/audit_log.go | 12 +++++++ manager/domain/interface.go | 64 +++++++++++++++++++++++++++++++++++++ manager/domain/value.go | 14 ++++++++ manager/repository/repo.go | 13 ++++++++ 7 files changed, 149 insertions(+) create mode 100644 manager/domain/account.go create mode 100644 manager/domain/audit_log.go create mode 100644 manager/domain/interface.go create mode 100644 manager/domain/value.go create mode 100644 manager/repository/repo.go diff --git a/go.mod b/go.mod index 79be3e6..81bdc49 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/rs/xid v1.5.0 github.com/stretchr/testify v1.10.0 + go.mongodb.org/mongo-driver/v2 v2.4.0 k8s.io/api v0.33.2 k8s.io/apimachinery v0.33.2 k8s.io/client-go v0.33.2 diff --git a/go.sum b/go.sum index 4da52e5..06e6679 100644 --- a/go.sum +++ b/go.sum @@ -146,6 +146,8 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.mongodb.org/mongo-driver/v2 v2.4.0 h1:Oq6BmUAAFTzMeh6AonuDlgZMuAuEiUxoAD1koK5MuFo= +go.mongodb.org/mongo-driver/v2 v2.4.0/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/manager/domain/account.go b/manager/domain/account.go new file mode 100644 index 0000000..1cb1f2d --- /dev/null +++ b/manager/domain/account.go @@ -0,0 +1,43 @@ +package domain + +import "go.mongodb.org/mongo-driver/v2/bson" + +type UserStatus int8 + +const ( + UserStatusActive UserStatus = 1 + UserStatusInactive UserStatus = 2 + UserStatusWaitChangePassword UserStatus = 3 + UserStatusBanned UserStatus = 4 +) + +type User struct { + BaseEntity + Email string `bson:"email,omitempty"` + Password EncryptedPassword `bson:"password,omitempty"` + Status UserStatus `bson:"status,omitempty"` + RoleIDs []bson.ObjectID `bson:"_id,omitempty"` + PermissionKeys []string `bson:"permission_keys,omitempty"` +} + +type Role struct { + BaseEntity + Name string `bson:"name,omitempty"` + Description string `bson:"description,omitempty"` + Policies []Policy `bson:"policies,omitempty"` +} + +type Policy struct { + PermissionKey string `bson:"permission_key,omitempty"` + Self bool `bson:"self,omitempty"` + K8SNamespace string `bson:"k8s_namespace,omitempty"` + PolicyNamespace string `bson:"policy_namespace,omitempty"` +} + +type Permission struct { + BaseEntity + Key string `bson:"key,omitempty"` + Description string `bson:"description,omitempty"` + Resource string `bson:"resource,omitempty"` + Action string `bson:"action,omitempty"` +} diff --git a/manager/domain/audit_log.go b/manager/domain/audit_log.go new file mode 100644 index 0000000..7b294cc --- /dev/null +++ b/manager/domain/audit_log.go @@ -0,0 +1,12 @@ +package domain + +import "go.mongodb.org/mongo-driver/v2/bson" + +type AuditLog struct { + ID bson.ObjectID `bson:"_id,omitempty"` + UserID bson.ObjectID `bson:"user_id,omitempty"` + Action string `bson:"action,omitempty"` + RequestID string `bson:"request_id,omitempty"` + Timestamp int64 `bson:"timestamp,omitempty"` + IP string `bson:"ip,omitempty"` +} diff --git a/manager/domain/interface.go b/manager/domain/interface.go new file mode 100644 index 0000000..3c874e5 --- /dev/null +++ b/manager/domain/interface.go @@ -0,0 +1,64 @@ +package domain + +import ( + "context" + + "go.mongodb.org/mongo-driver/v2/bson" +) + +type QueryUserOptions struct { + IDs []bson.ObjectID + Email string + Result []*User +} + +type QueryRoleOptions struct { + IDs []bson.ObjectID + Names []string + Result []*Role +} + +type QueryPermissionOptions struct { + IDs []bson.ObjectID + Keys []string + Resources []string + Result []*Permission +} + +type QueryAuditLogOptions struct { + TimestampGTE int64 + TimestampLTE int64 + UserIDs []bson.ObjectID + Result []*AuditLog +} + +type Repository interface { + CreateUser(ctx context.Context, user *User) error + UpdateUser(ctx context.Context, user *User) error + QueryUsers(ctx context.Context, opt *QueryUserOptions) error + CreateRole(ctx context.Context, role *Role) error + UpdateRole(ctx context.Context, role *Role) error + QueryRoles(ctx context.Context, opt *QueryRoleOptions) error + CreatePermission(ctx context.Context, permission *Permission) error + UpdatePermission(ctx context.Context, permission *Permission) error + QueryPermissions(ctx context.Context, opt *QueryPermissionOptions) error + CreateAuditLog(ctx context.Context, log *AuditLog) error + QueryAuditLogs(ctx context.Context, opt *QueryAuditLogOptions) error +} + +type Service interface { + SignUp(ctx context.Context, email, password string) error + Login(ctx context.Context, email, password string) (string, error) + Logout(ctx context.Context, token string) error + ChangePassword(ctx context.Context, email string, oldPassword, newPassword EncryptedPassword) error + CreateUser(ctx context.Context, user *User) error + DeleteUser(ctx context.Context, userID bson.ObjectID) error + UpdateUser(ctx context.Context, user *User) error + ListAuditLogs(ctx context.Context, opt *QueryAuditLogOptions) error + CreateRole(ctx context.Context, role *Role) error + UpdateRole(ctx context.Context, role *Role) error + QueryRoles(ctx context.Context, opt *QueryRoleOptions) error + CreatePermission(ctx context.Context, permission *Permission) error + UpdatePermission(ctx context.Context, permission *Permission) error + QueryPermissions(ctx context.Context, opt *QueryPermissionOptions) error +} diff --git a/manager/domain/value.go b/manager/domain/value.go new file mode 100644 index 0000000..9905339 --- /dev/null +++ b/manager/domain/value.go @@ -0,0 +1,14 @@ +package domain + +import "go.mongodb.org/mongo-driver/v2/bson" + +type EncryptedPassword string + +type BaseEntity struct { + ID bson.ObjectID `bson:"_id,omitempty"` + CreatedTime int64 `bson:"created_time,omitempty"` + UpdatedTime int64 `bson:"updated_time,omitempty"` + DeletedTime int64 `bson:"deleted_time,omitempty"` + CreatorID bson.ObjectID `bson:"creator_id,omitempty"` + UpdaterID bson.ObjectID `bson:"updater_id,omitempty"` +} diff --git a/manager/repository/repo.go b/manager/repository/repo.go new file mode 100644 index 0000000..9d284da --- /dev/null +++ b/manager/repository/repo.go @@ -0,0 +1,13 @@ +package repository + +import "github.com/Gthulhu/api/manager/domain" + +type Params struct { +} + +func NewRepository(params Params) (domain.Repository, error) { + return nil, nil +} + +type repo struct { +} From 5d6d458e6a75da21918f63a03e66dcedd78ccc61 Mon Sep 17 00:00:00 2001 From: Yanun Date: Sat, 22 Nov 2025 22:56:03 +0800 Subject: [PATCH 03/18] feat: impl repository interface --- go.mod | 7 + go.sum | 29 ++++ manager/domain/errors.go | 7 + manager/repository/repo.go | 302 ++++++++++++++++++++++++++++++++++++- 4 files changed, 343 insertions(+), 2 deletions(-) create mode 100644 manager/domain/errors.go diff --git a/go.mod b/go.mod index 81bdc49..e045257 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/snappy v1.0.0 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect @@ -39,6 +40,7 @@ require ( github.com/jedib0t/go-pretty/v6 v6.6.7 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.16.7 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect github.com/knadh/koanf/parsers/yaml v0.1.0 // indirect github.com/knadh/koanf/providers/env v1.0.0 // indirect @@ -63,9 +65,14 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/vektra/mockery/v3 v3.5.5 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + golang.org/x/crypto v0.41.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.27.0 // indirect golang.org/x/net v0.43.0 // indirect diff --git a/go.sum b/go.sum index 06e6679..bd667f7 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,8 @@ 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/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -58,6 +60,8 @@ 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/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= @@ -138,14 +142,23 @@ github.com/vektra/mockery/v3 v3.5.5 h1:1ExE+yqz3ytvEOe7pUH5VWIwmsYlSq+FjWPVVLdE8 github.com/vektra/mockery/v3 v3.5.5/go.mod h1:Oti3Df0WP8wwT31yuVri3QNsDeMUQU5Q4QEg8EabaBw= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= 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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver/v2 v2.4.0 h1:Oq6BmUAAFTzMeh6AonuDlgZMuAuEiUxoAD1koK5MuFo= go.mongodb.org/mongo-driver/v2 v2.4.0/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -153,16 +166,22 @@ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 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-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.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= @@ -170,20 +189,29 @@ golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT 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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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-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-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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +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.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= 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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= @@ -192,6 +220,7 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/manager/domain/errors.go b/manager/domain/errors.go new file mode 100644 index 0000000..d6db21e --- /dev/null +++ b/manager/domain/errors.go @@ -0,0 +1,7 @@ +package domain + +import "errors" + +var ( + ErrNotFound = errors.New("not found") +) diff --git a/manager/repository/repo.go b/manager/repository/repo.go index 9d284da..40bb5f6 100644 --- a/manager/repository/repo.go +++ b/manager/repository/repo.go @@ -1,13 +1,311 @@ package repository -import "github.com/Gthulhu/api/manager/domain" +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/Gthulhu/api/manager/domain" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" +) type Params struct { + Client *mongo.Client + Database string } func NewRepository(params Params) (domain.Repository, error) { - return nil, nil + if params.Client == nil { + return nil, errors.New("mongo client is required") + } + + dbName := params.Database + if dbName == "" { + dbName = "manager" + } + + return &repo{ + client: params.Client, + db: params.Client.Database(dbName), + }, nil } type repo struct { + client *mongo.Client + db *mongo.Database +} + +const ( + userCollection = "users" + roleCollection = "roles" + permissionCollection = "permissions" + auditLogCollection = "audit_logs" + defaultTimestampField = "timestamp" +) + +func (r *repo) CreateUser(ctx context.Context, user *domain.User) error { + if user == nil { + return errors.New("user is nil") + } + + now := time.Now().Unix() + if user.ID.IsZero() { + user.ID = bson.NewObjectID() + } + if user.CreatedTime == 0 { + user.CreatedTime = now + } + user.UpdatedTime = now + + res, err := r.db.Collection(userCollection).InsertOne(ctx, user) + if err != nil { + return fmt.Errorf("create user, err: %w", err) + } + if oid, ok := res.InsertedID.(bson.ObjectID); ok { + user.ID = oid + } + return nil +} + +func (r *repo) UpdateUser(ctx context.Context, user *domain.User) error { + if user == nil { + return errors.New("user is nil") + } + if user.ID.IsZero() { + return errors.New("user id is required") + } + + user.UpdatedTime = time.Now().Unix() + res, err := r.db.Collection(userCollection).ReplaceOne(ctx, bson.M{"_id": user.ID}, user) + if err != nil { + return fmt.Errorf("update user, err: %w", err) + } + if res.MatchedCount == 0 { + return domain.ErrNotFound + } + return nil +} + +func (r *repo) QueryUsers(ctx context.Context, opt *domain.QueryUserOptions) error { + if opt == nil { + return errors.New("query options is nil") + } + + filter := bson.M{} + if len(opt.IDs) > 0 { + filter["_id"] = bson.M{"$in": opt.IDs} + } + if opt.Email != "" { + filter["email"] = opt.Email + } + + cursor, err := r.db.Collection(userCollection).Find(ctx, filter) + if err != nil { + return fmt.Errorf("find users, err: %w", err) + } + + var result []*domain.User + if err := cursor.All(ctx, &result); err != nil { + return fmt.Errorf("decode users, err: %w", err) + } + opt.Result = result + return nil +} + +func (r *repo) CreateRole(ctx context.Context, role *domain.Role) error { + if role == nil { + return errors.New("role is nil") + } + + now := time.Now().Unix() + if role.ID.IsZero() { + role.ID = bson.NewObjectID() + } + if role.CreatedTime == 0 { + role.CreatedTime = now + } + role.UpdatedTime = now + + res, err := r.db.Collection(roleCollection).InsertOne(ctx, role) + if err != nil { + return fmt.Errorf("create role, err: %w", err) + } + if oid, ok := res.InsertedID.(bson.ObjectID); ok { + role.ID = oid + } + return nil +} + +func (r *repo) UpdateRole(ctx context.Context, role *domain.Role) error { + if role == nil { + return errors.New("role is nil") + } + if role.ID.IsZero() { + return errors.New("role id is required") + } + + role.UpdatedTime = time.Now().Unix() + res, err := r.db.Collection(roleCollection).ReplaceOne(ctx, bson.M{"_id": role.ID}, role) + if err != nil { + return fmt.Errorf("update role, err: %w", err) + } + if res.MatchedCount == 0 { + return domain.ErrNotFound + } + return nil +} + +func (r *repo) QueryRoles(ctx context.Context, opt *domain.QueryRoleOptions) error { + if opt == nil { + return errors.New("query options is nil") + } + + filter := bson.M{} + if len(opt.IDs) > 0 { + filter["_id"] = bson.M{"$in": opt.IDs} + } + if len(opt.Names) > 0 { + filter["name"] = bson.M{"$in": opt.Names} + } + + cursor, err := r.db.Collection(roleCollection).Find(ctx, filter) + if err != nil { + return fmt.Errorf("find roles, err: %w", err) + } + + var result []*domain.Role + if err := cursor.All(ctx, &result); err != nil { + return fmt.Errorf("decode roles, err: %w", err) + } + opt.Result = result + return nil +} + +func (r *repo) CreatePermission(ctx context.Context, permission *domain.Permission) error { + if permission == nil { + return errors.New("permission is nil") + } + + now := time.Now().Unix() + if permission.ID.IsZero() { + permission.ID = bson.NewObjectID() + } + if permission.CreatedTime == 0 { + permission.CreatedTime = now + } + permission.UpdatedTime = now + + res, err := r.db.Collection(permissionCollection).InsertOne(ctx, permission) + if err != nil { + return fmt.Errorf("create permission, err: %w", err) + } + if oid, ok := res.InsertedID.(bson.ObjectID); ok { + permission.ID = oid + } + return nil +} + +func (r *repo) UpdatePermission(ctx context.Context, permission *domain.Permission) error { + if permission == nil { + return errors.New("permission is nil") + } + if permission.ID.IsZero() { + return errors.New("permission id is required") + } + + permission.UpdatedTime = time.Now().Unix() + res, err := r.db.Collection(permissionCollection).ReplaceOne(ctx, bson.M{"_id": permission.ID}, permission) + if err != nil { + return fmt.Errorf("update permission, err: %w", err) + } + if res.MatchedCount == 0 { + return domain.ErrNotFound + } + return nil +} + +func (r *repo) QueryPermissions(ctx context.Context, opt *domain.QueryPermissionOptions) error { + if opt == nil { + return errors.New("query options is nil") + } + + filter := bson.M{} + if len(opt.IDs) > 0 { + filter["_id"] = bson.M{"$in": opt.IDs} + } + if len(opt.Keys) > 0 { + filter["key"] = bson.M{"$in": opt.Keys} + } + if len(opt.Resources) > 0 { + filter["resource"] = bson.M{"$in": opt.Resources} + } + + cursor, err := r.db.Collection(permissionCollection).Find(ctx, filter) + if err != nil { + return fmt.Errorf("find permissions, err: %w", err) + } + + var result []*domain.Permission + if err := cursor.All(ctx, &result); err != nil { + return fmt.Errorf("decode permissions, err: %w", err) + } + opt.Result = result + return nil +} + +func (r *repo) CreateAuditLog(ctx context.Context, log *domain.AuditLog) error { + if log == nil { + return errors.New("audit log is nil") + } + if log.ID.IsZero() { + log.ID = bson.NewObjectID() + } + if log.Timestamp == 0 { + log.Timestamp = time.Now().Unix() + } + + res, err := r.db.Collection(auditLogCollection).InsertOne(ctx, log) + if err != nil { + return fmt.Errorf("create audit log, err: %w", err) + } + if oid, ok := res.InsertedID.(bson.ObjectID); ok { + log.ID = oid + } + return nil +} + +func (r *repo) QueryAuditLogs(ctx context.Context, opt *domain.QueryAuditLogOptions) error { + if opt == nil { + return errors.New("query options is nil") + } + + filter := bson.M{} + if len(opt.UserIDs) > 0 { + filter["user_id"] = bson.M{"$in": opt.UserIDs} + } + + if opt.TimestampGTE > 0 || opt.TimestampLTE > 0 { + timeFilter := bson.M{} + if opt.TimestampGTE > 0 { + timeFilter["$gte"] = opt.TimestampGTE + } + if opt.TimestampLTE > 0 { + timeFilter["$lte"] = opt.TimestampLTE + } + filter[defaultTimestampField] = timeFilter + } + + cursor, err := r.db.Collection(auditLogCollection).Find(ctx, filter) + if err != nil { + return fmt.Errorf("find audit logs, err: %w", err) + } + + var result []*domain.AuditLog + if err := cursor.All(ctx, &result); err != nil { + return fmt.Errorf("decode audit logs, err: %w", err) + } + opt.Result = result + return nil } From 5f0eefe8543bb1b58a3f7a6e44774de3723c47dd Mon Sep 17 00:00:00 2001 From: Yanun Date: Sun, 23 Nov 2025 10:17:57 +0800 Subject: [PATCH 04/18] fix: repalce unix with unixmilli --- manager/repository/repo.go | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/manager/repository/repo.go b/manager/repository/repo.go index 40bb5f6..47b9900 100644 --- a/manager/repository/repo.go +++ b/manager/repository/repo.go @@ -47,10 +47,10 @@ const ( func (r *repo) CreateUser(ctx context.Context, user *domain.User) error { if user == nil { - return errors.New("user is nil") + return errors.New("nil user") } - now := time.Now().Unix() + now := time.Now().UnixMilli() if user.ID.IsZero() { user.ID = bson.NewObjectID() } @@ -71,13 +71,13 @@ func (r *repo) CreateUser(ctx context.Context, user *domain.User) error { func (r *repo) UpdateUser(ctx context.Context, user *domain.User) error { if user == nil { - return errors.New("user is nil") + return errors.New("nil user") } if user.ID.IsZero() { return errors.New("user id is required") } - user.UpdatedTime = time.Now().Unix() + user.UpdatedTime = time.Now().UnixMilli() res, err := r.db.Collection(userCollection).ReplaceOne(ctx, bson.M{"_id": user.ID}, user) if err != nil { return fmt.Errorf("update user, err: %w", err) @@ -90,7 +90,7 @@ func (r *repo) UpdateUser(ctx context.Context, user *domain.User) error { func (r *repo) QueryUsers(ctx context.Context, opt *domain.QueryUserOptions) error { if opt == nil { - return errors.New("query options is nil") + return errors.New("nil query options") } filter := bson.M{} @@ -116,10 +116,10 @@ func (r *repo) QueryUsers(ctx context.Context, opt *domain.QueryUserOptions) err func (r *repo) CreateRole(ctx context.Context, role *domain.Role) error { if role == nil { - return errors.New("role is nil") + return errors.New("nil role") } - now := time.Now().Unix() + now := time.Now().UnixMilli() if role.ID.IsZero() { role.ID = bson.NewObjectID() } @@ -140,13 +140,13 @@ func (r *repo) CreateRole(ctx context.Context, role *domain.Role) error { func (r *repo) UpdateRole(ctx context.Context, role *domain.Role) error { if role == nil { - return errors.New("role is nil") + return errors.New("nil role") } if role.ID.IsZero() { return errors.New("role id is required") } - role.UpdatedTime = time.Now().Unix() + role.UpdatedTime = time.Now().UnixMilli() res, err := r.db.Collection(roleCollection).ReplaceOne(ctx, bson.M{"_id": role.ID}, role) if err != nil { return fmt.Errorf("update role, err: %w", err) @@ -159,7 +159,7 @@ func (r *repo) UpdateRole(ctx context.Context, role *domain.Role) error { func (r *repo) QueryRoles(ctx context.Context, opt *domain.QueryRoleOptions) error { if opt == nil { - return errors.New("query options is nil") + return errors.New("nil query options") } filter := bson.M{} @@ -185,10 +185,10 @@ func (r *repo) QueryRoles(ctx context.Context, opt *domain.QueryRoleOptions) err func (r *repo) CreatePermission(ctx context.Context, permission *domain.Permission) error { if permission == nil { - return errors.New("permission is nil") + return errors.New("nil permission") } - now := time.Now().Unix() + now := time.Now().UnixMilli() if permission.ID.IsZero() { permission.ID = bson.NewObjectID() } @@ -209,13 +209,13 @@ func (r *repo) CreatePermission(ctx context.Context, permission *domain.Permissi func (r *repo) UpdatePermission(ctx context.Context, permission *domain.Permission) error { if permission == nil { - return errors.New("permission is nil") + return errors.New("nil permission") } if permission.ID.IsZero() { return errors.New("permission id is required") } - permission.UpdatedTime = time.Now().Unix() + permission.UpdatedTime = time.Now().UnixMilli() res, err := r.db.Collection(permissionCollection).ReplaceOne(ctx, bson.M{"_id": permission.ID}, permission) if err != nil { return fmt.Errorf("update permission, err: %w", err) @@ -228,7 +228,7 @@ func (r *repo) UpdatePermission(ctx context.Context, permission *domain.Permissi func (r *repo) QueryPermissions(ctx context.Context, opt *domain.QueryPermissionOptions) error { if opt == nil { - return errors.New("query options is nil") + return errors.New("nil query options") } filter := bson.M{} @@ -257,13 +257,13 @@ func (r *repo) QueryPermissions(ctx context.Context, opt *domain.QueryPermission func (r *repo) CreateAuditLog(ctx context.Context, log *domain.AuditLog) error { if log == nil { - return errors.New("audit log is nil") + return errors.New("nil audit log") } if log.ID.IsZero() { log.ID = bson.NewObjectID() } if log.Timestamp == 0 { - log.Timestamp = time.Now().Unix() + log.Timestamp = time.Now().UnixMilli() } res, err := r.db.Collection(auditLogCollection).InsertOne(ctx, log) @@ -278,7 +278,7 @@ func (r *repo) CreateAuditLog(ctx context.Context, log *domain.AuditLog) error { func (r *repo) QueryAuditLogs(ctx context.Context, opt *domain.QueryAuditLogOptions) error { if opt == nil { - return errors.New("query options is nil") + return errors.New("nil query options") } filter := bson.M{} From cd223968579ea0f98bb4cf2e878e0d9f0c72873b Mon Sep 17 00:00:00 2001 From: Vic Xu Date: Sun, 23 Nov 2025 20:52:00 +0800 Subject: [PATCH 05/18] feature/account: implement auth service and user RUD service --- go.mod | 16 ++--- go.sum | 14 +++++ manager/domain/auth.go | 10 ++++ manager/domain/interface.go | 6 +- manager/domain/value.go | 45 +++++++++++++- manager/service/auth_svc.go | 114 ++++++++++++++++++++++++++++++++++++ manager/service/svc.go | 12 ++++ manager/service/user_svc.go | 53 +++++++++++++++++ pkg/logger/logger.go | 24 ++++++++ pkg/util/encrypt.go | 108 ++++++++++++++++++++++++++++++++++ pkg/util/encrypt_test.go | 24 ++++++++ pkg/util/logger.go | 24 ++++++++ pkg/util/ptr.go | 12 ++++ util/logger.go | 17 ++++++ 14 files changed, 467 insertions(+), 12 deletions(-) create mode 100644 manager/domain/auth.go create mode 100644 manager/service/auth_svc.go create mode 100644 manager/service/svc.go create mode 100644 manager/service/user_svc.go create mode 100644 pkg/logger/logger.go create mode 100644 pkg/util/encrypt.go create mode 100644 pkg/util/encrypt_test.go create mode 100644 pkg/util/logger.go create mode 100644 pkg/util/ptr.go diff --git a/go.mod b/go.mod index e045257..80636ff 100644 --- a/go.mod +++ b/go.mod @@ -72,17 +72,17 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect - golang.org/x/crypto v0.41.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/mod v0.27.0 // indirect - golang.org/x/net v0.43.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/term v0.34.0 // indirect - golang.org/x/text v0.28.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.9.0 // indirect - golang.org/x/tools v0.36.0 // indirect + golang.org/x/tools v0.38.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index bd667f7..37a38a1 100644 --- a/go.sum +++ b/go.sum @@ -169,6 +169,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -176,6 +178,7 @@ golang.org/x/mod v0.3.0/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.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -184,6 +187,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -192,6 +197,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -204,16 +211,22 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= 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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -223,6 +236,7 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/manager/domain/auth.go b/manager/domain/auth.go new file mode 100644 index 0000000..6161a1b --- /dev/null +++ b/manager/domain/auth.go @@ -0,0 +1,10 @@ +package domain + +import "github.com/golang-jwt/jwt/v5" + +// Claims represents JWT token claims +type Claims struct { + UID string `json:"uid"` + Roles []string `json:"roles"` + jwt.RegisteredClaims +} diff --git a/manager/domain/interface.go b/manager/domain/interface.go index 3c874e5..8b412a9 100644 --- a/manager/domain/interface.go +++ b/manager/domain/interface.go @@ -51,9 +51,9 @@ type Service interface { Login(ctx context.Context, email, password string) (string, error) Logout(ctx context.Context, token string) error ChangePassword(ctx context.Context, email string, oldPassword, newPassword EncryptedPassword) error - CreateUser(ctx context.Context, user *User) error - DeleteUser(ctx context.Context, userID bson.ObjectID) error - UpdateUser(ctx context.Context, user *User) error + CreateUser(ctx context.Context, operator Claims, user *User) error + DeleteUser(ctx context.Context, operator Claims, userID bson.ObjectID) error + UpdateUser(ctx context.Context, operator Claims, user *User) error ListAuditLogs(ctx context.Context, opt *QueryAuditLogOptions) error CreateRole(ctx context.Context, role *Role) error UpdateRole(ctx context.Context, role *Role) error diff --git a/manager/domain/value.go b/manager/domain/value.go index 9905339..0dce7ce 100644 --- a/manager/domain/value.go +++ b/manager/domain/value.go @@ -1,9 +1,36 @@ package domain -import "go.mongodb.org/mongo-driver/v2/bson" +import ( + "time" + + "github.com/Gthulhu/api/pkg/util" + "go.mongodb.org/mongo-driver/v2/bson" +) type EncryptedPassword string +func (value EncryptedPassword) MarshalBSONValue() (typ byte, data []byte, err error) { + pwdHash, err := util.CreateArgon2Hash(string(value)) + return byte(bson.TypeString), []byte(pwdHash), err +} + +func (value *EncryptedPassword) UnmarshalBSONValue(typ byte, data []byte) error { + *value = EncryptedPassword(string(data)) + return nil +} + +func (value EncryptedPassword) String() string { + return "*******" +} + +func (value EncryptedPassword) Cmp(plainText string) (bool, error) { + ok, err := util.ComparePasswordAndHash(plainText, string(value)) + if err != nil { + return false, err + } + return ok, nil +} + type BaseEntity struct { ID bson.ObjectID `bson:"_id,omitempty"` CreatedTime int64 `bson:"created_time,omitempty"` @@ -12,3 +39,19 @@ type BaseEntity struct { CreatorID bson.ObjectID `bson:"creator_id,omitempty"` UpdaterID bson.ObjectID `bson:"updater_id,omitempty"` } + +func NewBaseEntity(creatorID, updaterID *bson.ObjectID) BaseEntity { + nowInmsec := time.Now().UnixMilli() + entity := BaseEntity{ + CreatedTime: nowInmsec, + UpdatedTime: nowInmsec, + DeletedTime: 0, + } + if creatorID != nil { + entity.CreatorID = *creatorID + } + if updaterID != nil { + entity.UpdaterID = *updaterID + } + return entity +} diff --git a/manager/service/auth_svc.go b/manager/service/auth_svc.go new file mode 100644 index 0000000..7871016 --- /dev/null +++ b/manager/service/auth_svc.go @@ -0,0 +1,114 @@ +package service + +import ( + "context" + "fmt" + "time" + + "github.com/Gthulhu/api/manager/domain" + "github.com/Gthulhu/api/pkg/util" + "github.com/golang-jwt/jwt/v5" + "go.mongodb.org/mongo-driver/v2/bson" +) + +func (svc *Service) SignUp(ctx context.Context, email, password string) error { + creatorID := bson.NewObjectID() + user := domain.User{ + BaseEntity: domain.NewBaseEntity(util.Ptr(creatorID), util.Ptr(creatorID)), + Email: email, + Password: domain.EncryptedPassword(password), + Status: domain.UserStatusWaitChangePassword, + } + err := svc.Repo.CreateUser(ctx, &user) + if err != nil { + // TODO: handle duplicate email error + return err + } + + return nil +} + +func (svc *Service) Login(ctx context.Context, email, password string) (string, error) { + user, err := svc.getUserByEmaiL(ctx, email) + if err != nil { + return "", err + } + ok, err := user.Password.Cmp(password) + if err != nil { + return "", err + } + if !ok { + return "", fmt.Errorf("invalid password") + } + token, err := svc.genJWTToken(ctx, user) + if err != nil { + return "", err + } + return token, nil +} + +func (svc *Service) Logout(ctx context.Context, token string) error { + return nil +} + +func (svc *Service) ChangePassword(ctx context.Context, email string, oldPassword, newPassword string) error { + user, err := svc.getUserByEmaiL(ctx, email) + if err != nil { + return err + } + ok, err := user.Password.Cmp(oldPassword) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("invalid old password") + } + user.Password = domain.EncryptedPassword(newPassword) + user.UpdatedTime = time.Now().UnixMilli() + err = svc.Repo.UpdateUser(ctx, user) + if err != nil { + return err + } + return nil +} + +func (svc *Service) getUserByEmaiL(ctx context.Context, email string) (*domain.User, error) { + opts := &domain.QueryUserOptions{ + Email: email, + } + err := svc.Repo.QueryUsers(ctx, opts) + if err != nil { + return nil, err + } + users := opts.Result + if len(users) == 0 { + // TODO: return specific not found error + return nil, fmt.Errorf("user with email %s not found", email) + } + + return users[0], nil +} + +func (svc *Service) genJWTToken(ctx context.Context, user *domain.User) (string, error) { + tokenTTL := time.Duration(3) * time.Hour + uid := user.ID.Hex() + + roles := []string{} + for _, roleID := range user.RoleIDs { + roles = append(roles, roleID.Hex()) + } + claims := domain.Claims{ + UID: uid, + Roles: roles, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(tokenTTL)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Issuer: "bss-api-server", + Subject: uid, + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + return token.SignedString(svc.jwtPrivateKey) +} diff --git a/manager/service/svc.go b/manager/service/svc.go new file mode 100644 index 0000000..2844767 --- /dev/null +++ b/manager/service/svc.go @@ -0,0 +1,12 @@ +package service + +import ( + "crypto/rsa" + + "github.com/Gthulhu/api/manager/domain" +) + +type Service struct { + Repo domain.Repository + jwtPrivateKey *rsa.PrivateKey +} diff --git a/manager/service/user_svc.go b/manager/service/user_svc.go new file mode 100644 index 0000000..81a98b3 --- /dev/null +++ b/manager/service/user_svc.go @@ -0,0 +1,53 @@ +package service + +import ( + "context" + "time" + + "github.com/Gthulhu/api/manager/domain" + "github.com/Gthulhu/api/pkg/util" + "go.mongodb.org/mongo-driver/v2/bson" +) + +func (svc *Service) CreateUser(ctx context.Context, operator domain.Claims, user *domain.User) error { + operatorID, err := bson.ObjectIDFromHex(operator.UID) + if err != nil { + return err + } + + user.BaseEntity = domain.NewBaseEntity(util.Ptr(operatorID), util.Ptr(operatorID)) + user.Status = domain.UserStatusWaitChangePassword + return svc.Repo.CreateUser(ctx, user) + +} + +func (svc *Service) DeleteUser(ctx context.Context, operator domain.Claims, userID bson.ObjectID) error { + operatorID, err := bson.ObjectIDFromHex(operator.UID) + if err != nil { + return err + } + updateUser := &domain.User{ + BaseEntity: domain.NewBaseEntity(nil, util.Ptr(operatorID)), + } + updateUser.ID = userID + updateUser.DeletedTime = time.Now().UnixMilli() + err = svc.Repo.UpdateUser(ctx, updateUser) + if err != nil { + return err + } + return nil +} + +func (svc *Service) UpdateUser(ctx context.Context, operator domain.Claims, user *domain.User) error { + operatorID, err := bson.ObjectIDFromHex(operator.UID) + if err != nil { + return err + } + user.UpdaterID = operatorID + user.UpdatedTime = time.Now().UnixMilli() + err = svc.Repo.UpdateUser(ctx, user) + if err != nil { + return err + } + return nil +} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go new file mode 100644 index 0000000..80044d5 --- /dev/null +++ b/pkg/logger/logger.go @@ -0,0 +1,24 @@ +package logger + +import ( + "context" + "os" + + "github.com/rs/zerolog" +) + +func InitLogger() *zerolog.Logger { + consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: "15:04:05"} + + logger := zerolog.New(consoleWriter). + With(). + Timestamp(). + Logger() + zerolog.SetGlobalLevel(zerolog.DebugLevel) + zerolog.DefaultContextLogger = &logger + return &logger +} + +func Logger(ctx context.Context) *zerolog.Logger { + return zerolog.Ctx(ctx) +} diff --git a/pkg/util/encrypt.go b/pkg/util/encrypt.go new file mode 100644 index 0000000..ce5e533 --- /dev/null +++ b/pkg/util/encrypt.go @@ -0,0 +1,108 @@ +package util + +import ( + "crypto/rand" + "crypto/subtle" + "encoding/base64" + "fmt" + "strings" + + "golang.org/x/crypto/argon2" +) + +type Argon2idParams struct { + Memory uint32 + Iterations uint32 + Parallelism uint8 + SaltLength uint32 + KeyLength uint32 +} + +var defaultArgon2idParams = Argon2idParams{ + Memory: 16 * 1024, + Iterations: 3, + Parallelism: 2, + SaltLength: 16, + KeyLength: 32, +} + +func InitArgon2idParams(param Argon2idParams) { + defaultArgon2idParams = param +} + +func CreateArgon2Hash(password string) (string, error) { + // 1. 產生隨機 Salt + p := defaultArgon2idParams + salt := make([]byte, p.SaltLength) + _, err := rand.Read(salt) + if err != nil { + return "", err + } + + // 2. 產生 Hash + hash := argon2.IDKey([]byte(password), salt, p.Iterations, p.Memory, p.Parallelism, p.KeyLength) + + // 3. 將 Salt 和 Hash 轉為 Base64 + b64Salt := base64.RawStdEncoding.EncodeToString(salt) + b64Hash := base64.RawStdEncoding.EncodeToString(hash) + + // 4. 組合成標準格式字串 return + encodedHash := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", + argon2.Version, p.Memory, p.Iterations, p.Parallelism, b64Salt, b64Hash) + + return encodedHash, nil +} + +func ComparePasswordAndHash(password, encodedHash string) (bool, error) { + // 1. 解析 Hash 字串 + p, salt, hash, err := decodeHash(encodedHash) + if err != nil { + return false, err + } + + // 2. 使用解析出來的參數和 Salt,對輸入的密碼進行同樣的 Hash 運算 + otherHash := argon2.IDKey([]byte(password), salt, p.Iterations, p.Memory, p.Parallelism, p.KeyLength) + + // 3. 比對兩個 Hash 是否一致 (使用 ConstantTimeCompare 防止時序攻擊) + if subtle.ConstantTimeCompare(hash, otherHash) == 1 { + return true, nil + } + return false, nil +} + +// decodeHash 解析儲存的 Hash 字串,還原參數、Salt 和原始 Hash +func decodeHash(encodedHash string) (p *Argon2idParams, salt, hash []byte, err error) { + vals := strings.Split(encodedHash, "$") + if len(vals) != 6 { + return nil, nil, nil, fmt.Errorf("無效的 hash 格式") + } + + var version int + _, err = fmt.Sscanf(vals[2], "v=%d", &version) + if err != nil { + return nil, nil, nil, err + } + if version != argon2.Version { + return nil, nil, nil, fmt.Errorf("不支援的 argon2 版本: %d", version) + } + + p = &Argon2idParams{} + _, err = fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", &p.Memory, &p.Iterations, &p.Parallelism) + if err != nil { + return nil, nil, nil, err + } + + salt, err = base64.RawStdEncoding.DecodeString(vals[4]) + if err != nil { + return nil, nil, nil, err + } + p.SaltLength = uint32(len(salt)) + + hash, err = base64.RawStdEncoding.DecodeString(vals[5]) + if err != nil { + return nil, nil, nil, err + } + p.KeyLength = uint32(len(hash)) + + return p, salt, hash, nil +} diff --git a/pkg/util/encrypt_test.go b/pkg/util/encrypt_test.go new file mode 100644 index 0000000..0dbb6f2 --- /dev/null +++ b/pkg/util/encrypt_test.go @@ -0,0 +1,24 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAngron(t *testing.T) { + password := "my_secure_password" + + hash, err := CreateArgon2Hash(password) + require.NoError(t, err) + + ok, err := ComparePasswordAndHash(password, hash) + require.NoError(t, err) + assert.True(t, ok, "Password should match the hash") + + wrongPassword := "wrong_password" + ok, err = ComparePasswordAndHash(wrongPassword, hash) + require.NoError(t, err) + assert.False(t, ok, "Wrong password should not match the hash") +} diff --git a/pkg/util/logger.go b/pkg/util/logger.go new file mode 100644 index 0000000..d42bf5c --- /dev/null +++ b/pkg/util/logger.go @@ -0,0 +1,24 @@ +package util + +import ( + "context" + "os" + + "github.com/rs/zerolog" +) + +func InitLogger() *zerolog.Logger { + consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: "15:04:05"} + + logger := zerolog.New(consoleWriter). + With(). + Timestamp(). + Logger() + zerolog.SetGlobalLevel(zerolog.DebugLevel) + zerolog.DefaultContextLogger = &logger + return &logger +} + +func Logger(ctx context.Context) *zerolog.Logger { + return zerolog.Ctx(ctx) +} diff --git a/pkg/util/ptr.go b/pkg/util/ptr.go new file mode 100644 index 0000000..a178918 --- /dev/null +++ b/pkg/util/ptr.go @@ -0,0 +1,12 @@ +package util + +func Ptr[T any](v T) *T { + return &v +} + +func DerefPtr[T any](p *T, defaultValue T) T { + if p == nil { + return defaultValue + } + return *p +} diff --git a/util/logger.go b/util/logger.go index c0e0001..872ac99 100644 --- a/util/logger.go +++ b/util/logger.go @@ -6,6 +6,7 @@ import ( "os" "github.com/pkg/errors" + "github.com/rs/zerolog" ) var logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) @@ -33,3 +34,19 @@ func GetLogger() *slog.Logger { func LogErrAttr(err error) slog.Attr { return slog.String("error", errors.WithStack(err).Error()) } + +func InitLogger() *zerolog.Logger { + consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: "15:04:05"} + + logger := zerolog.New(consoleWriter). + With(). + Timestamp(). + Logger() + zerolog.SetGlobalLevel(zerolog.DebugLevel) + zerolog.DefaultContextLogger = &logger + return &logger +} + +func Logger(ctx context.Context) *zerolog.Logger { + return zerolog.Ctx(ctx) +} From 43ed909ceacaf63e965fa47a1d0963afee59de4a Mon Sep 17 00:00:00 2001 From: Vic Xu Date: Tue, 25 Nov 2025 08:26:14 +0800 Subject: [PATCH 06/18] feature/account: refactor project init process --- .gitignore | 3 +- Makefile | 8 + adapter/kubernetes/k8s_adapter.go | 121 ------ adapter/kubernetes/mock_kubernetes.go | 152 ------- cache/cache_test.go | 291 ------------- cache/pod_watcher.go | 114 ----- cache/strategy_cache.go | 457 --------------------- config/config.go | 188 --------- config/config.json | 31 -- config/manager_config.default.toml | 33 ++ config/manager_config.go | 85 ++++ config/manager_config.test.toml | 71 ++++ {k8s => deployment/k8s}/deployment.yaml | 0 deployment/local/docker-compose.infra.yaml | 13 + domain/interface.go | 23 -- domain/metrics.go | 19 - domain/pod.go | 17 - domain/strategy.go | 16 - go.mod | 66 ++- go.sum | 176 ++------ main.go | 149 +++---- manager/app/module.go | 73 ++++ manager/app/rest_app.go | 77 ++++ manager/cmd/.keep | 0 manager/domain/.keep | 0 manager/domain/interface.go | 2 +- manager/repository/.keep | 0 manager/repository/repo.go | 40 +- manager/rest/.keep | 0 manager/rest/auth_hdl.go | 23 ++ rest/handler.go => manager/rest/hander.go | 43 +- manager/rest/handler_test.go | 63 +++ manager/rest/routes.go | 36 ++ manager/service/.keep | 0 manager/service/svc.go | 72 ++++ options.go | 68 --- rest/metrics_hdl.go | 116 ------ rest/middleware.go | 188 --------- rest/pod_hdl.go | 36 -- rest/routes.go | 52 --- rest/strategies_hdl.go | 72 ---- rest/token_hdl.go | 46 --- service/auth_svc.go | 75 ---- service/auth_svc_test.go | 55 --- service/metrics_svc.go | 32 -- service/pods_svc.go | 162 -------- service/pods_svc_test.go | 56 --- service/strategies_svc.go | 131 ------ service/strategies_svc_test.go | 56 --- service/svc.go | 63 --- util/logger.go | 52 --- {static => web/static}/app.js | 0 {static => web/static}/index.html | 0 {static => web/static}/style.css | 0 54 files changed, 724 insertions(+), 2998 deletions(-) delete mode 100644 adapter/kubernetes/k8s_adapter.go delete mode 100644 adapter/kubernetes/mock_kubernetes.go delete mode 100644 cache/cache_test.go delete mode 100644 cache/pod_watcher.go delete mode 100644 cache/strategy_cache.go delete mode 100644 config/config.go delete mode 100644 config/config.json create mode 100644 config/manager_config.default.toml create mode 100644 config/manager_config.go create mode 100644 config/manager_config.test.toml rename {k8s => deployment/k8s}/deployment.yaml (100%) create mode 100644 deployment/local/docker-compose.infra.yaml delete mode 100644 domain/interface.go delete mode 100644 domain/metrics.go delete mode 100644 domain/pod.go delete mode 100644 domain/strategy.go create mode 100644 manager/app/module.go create mode 100644 manager/app/rest_app.go delete mode 100644 manager/cmd/.keep delete mode 100644 manager/domain/.keep delete mode 100644 manager/repository/.keep delete mode 100644 manager/rest/.keep create mode 100644 manager/rest/auth_hdl.go rename rest/handler.go => manager/rest/hander.go (63%) create mode 100644 manager/rest/handler_test.go create mode 100644 manager/rest/routes.go delete mode 100644 manager/service/.keep delete mode 100644 options.go delete mode 100644 rest/metrics_hdl.go delete mode 100644 rest/middleware.go delete mode 100644 rest/pod_hdl.go delete mode 100644 rest/routes.go delete mode 100644 rest/strategies_hdl.go delete mode 100644 rest/token_hdl.go delete mode 100644 service/auth_svc.go delete mode 100644 service/auth_svc_test.go delete mode 100644 service/metrics_svc.go delete mode 100644 service/pods_svc.go delete mode 100644 service/pods_svc_test.go delete mode 100644 service/strategies_svc.go delete mode 100644 service/strategies_svc_test.go delete mode 100644 service/svc.go delete mode 100644 util/logger.go rename {static => web/static}/app.js (100%) rename {static => web/static}/index.html (100%) rename {static => web/static}/style.css (100%) diff --git a/.gitignore b/.gitignore index 236578b..e41b991 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ bin -api \ No newline at end of file +api +config/manager_config.toml \ No newline at end of file diff --git a/Makefile b/Makefile index 2abd67c..c91102e 100644 --- a/Makefile +++ b/Makefile @@ -81,3 +81,11 @@ dev-setup: deps go install github.com/gorilla/mux@latest .DEFAULT_GOAL := help + +local-infra-up: + @echo "Starting local infrastructure with Docker Compose..." + docker-compose -f $(CURDIR)/deployment/local/docker-compose.infra.yaml up -d + +local-run-manager: + @echo "Running Manager locally..." + go run main.go manager --config-dir $(CURDIR)/config/manager_config.toml --config-name manager_config \ No newline at end of file diff --git a/adapter/kubernetes/k8s_adapter.go b/adapter/kubernetes/k8s_adapter.go deleted file mode 100644 index 44089be..0000000 --- a/adapter/kubernetes/k8s_adapter.go +++ /dev/null @@ -1,121 +0,0 @@ -package kubernetes - -import ( - "context" - "errors" - "fmt" - "log" - "log/slog" - "time" - - "github.com/Gthulhu/api/util" - apiv1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" -) - -// Define error types -var ( - ErrNoKubeConfig = errors.New("no Kubernetes configuration available") - ErrKubeClientNotInit = errors.New("Kubernetes client not initialized") - ErrNamespaceAccess = errors.New("failed to access Kubernetes namespaces") - ErrPodAccess = errors.New("failed to access Kubernetes pods") - ErrPodNotFound = errors.New("pod not found in any namespace") -) - -type K8sAdapter interface { - GetPodByPodUID(ctx context.Context, podUID string) (apiv1.Pod, error) - GetClient() *kubernetes.Clientset -} - -type k8sClient struct { - kubeClient *kubernetes.Clientset -} - -func (k *k8sClient) GetPodByPodUID(ctx context.Context, podUID string) (apiv1.Pod, error) { - namespaces, err := k.kubeClient.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) - if err != nil { - log.Printf("Error listing namespaces: %v", err) - return apiv1.Pod{}, fmt.Errorf("%w: %v", ErrNamespaceAccess, err) - } - - // Find the Pod that matches the UID in all namespaces - for _, ns := range namespaces.Items { - pods, err := k.kubeClient.CoreV1().Pods(ns.Name).List(ctx, metav1.ListOptions{}) - if err != nil { - log.Printf("Error listing pods in namespace %s: %v", ns.Name, err) - continue - } - - for _, pod := range pods.Items { - // Compare Pod UID - if string(pod.UID) == podUID { - // Update cache - // TODO: implement caching - // podLabelCacheMu.Lock() - // podLabelCache[podUID] = pod - // podLabelCacheTime[podUID] = time.Now() - // podLabelCacheMu.Unlock() - - log.Printf("Found and cached labels for pod %s in namespace %s", podUID, ns.Name) - return pod, nil - } - } - } - - return apiv1.Pod{}, ErrPodNotFound -} - -func (k *k8sClient) GetClient() *kubernetes.Clientset { - return k.kubeClient -} - -// Options contains Kubernetes adapter options -type Options struct { - KubeConfigPath string - InCluster bool -} - -// NewK8SAdapter creates a new Kubernetes adapter based on command line options. -// Supports two modes: -// 1. When running inside the cluster, use in-cluster configuration -// 2. When running outside the cluster, use kubeconfig configuration -func NewK8SAdapter(options Options) (K8sAdapter, error) { - - var config *rest.Config - var err error - - // Decide which configuration to use based on command line options - if options.InCluster { - // Use in-cluster configuration - util.GetLogger().Info("Using in-cluster Kubernetes configuration") - config, err = rest.InClusterConfig() - if err != nil { - return nil, fmt.Errorf("failed to create in-cluster config: %w", err) - } - } else if options.KubeConfigPath != "" { - // Use the specified kubeconfig file - util.GetLogger().Info("Using Kubernetes config", slog.String("path", options.KubeConfigPath)) - config, err = clientcmd.BuildConfigFromFlags("", options.KubeConfigPath) - if err != nil { - return nil, fmt.Errorf("failed to build kubeconfig from %s: %w", options.KubeConfigPath, err) - } - } else { - // Cannot access Kubernetes - return nil, ErrNoKubeConfig - } - - // Create Kubernetes client - config.Timeout = 10 * time.Second - config.QPS = 20 - config.Burst = 50 - - kubeClient, err := kubernetes.NewForConfig(config) - if err != nil { - return nil, fmt.Errorf("failed to create Kubernetes client: %w", err) - } - - return &k8sClient{kubeClient: kubeClient}, nil -} diff --git a/adapter/kubernetes/mock_kubernetes.go b/adapter/kubernetes/mock_kubernetes.go deleted file mode 100644 index 012ad64..0000000 --- a/adapter/kubernetes/mock_kubernetes.go +++ /dev/null @@ -1,152 +0,0 @@ -// Code generated by mockery; DO NOT EDIT. -// github.com/vektra/mockery -// template: testify - -package kubernetes - -import ( - "context" - - mock "github.com/stretchr/testify/mock" - "k8s.io/api/core/v1" - "k8s.io/client-go/kubernetes" -) - -// NewMockK8sAdapter creates a new instance of MockK8sAdapter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockK8sAdapter(t interface { - mock.TestingT - Cleanup(func()) -}) *MockK8sAdapter { - mock := &MockK8sAdapter{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// MockK8sAdapter is an autogenerated mock type for the K8sAdapter type -type MockK8sAdapter struct { - mock.Mock -} - -type MockK8sAdapter_Expecter struct { - mock *mock.Mock -} - -func (_m *MockK8sAdapter) EXPECT() *MockK8sAdapter_Expecter { - return &MockK8sAdapter_Expecter{mock: &_m.Mock} -} - -// GetClient provides a mock function for the type MockK8sAdapter -func (_mock *MockK8sAdapter) GetClient() *kubernetes.Clientset { - ret := _mock.Called() - - if len(ret) == 0 { - panic("no return value specified for GetClient") - } - - var r0 *kubernetes.Clientset - if returnFunc, ok := ret.Get(0).(func() *kubernetes.Clientset); ok { - r0 = returnFunc() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*kubernetes.Clientset) - } - } - return r0 -} - -// MockK8sAdapter_GetClient_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetClient' -type MockK8sAdapter_GetClient_Call struct { - *mock.Call -} - -// GetClient is a helper method to define mock.On call -func (_e *MockK8sAdapter_Expecter) GetClient() *MockK8sAdapter_GetClient_Call { - return &MockK8sAdapter_GetClient_Call{Call: _e.mock.On("GetClient")} -} - -func (_c *MockK8sAdapter_GetClient_Call) Run(run func()) *MockK8sAdapter_GetClient_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockK8sAdapter_GetClient_Call) Return(clientset *kubernetes.Clientset) *MockK8sAdapter_GetClient_Call { - _c.Call.Return(clientset) - return _c -} - -func (_c *MockK8sAdapter_GetClient_Call) RunAndReturn(run func() *kubernetes.Clientset) *MockK8sAdapter_GetClient_Call { - _c.Call.Return(run) - return _c -} - -// GetPodByPodUID provides a mock function for the type MockK8sAdapter -func (_mock *MockK8sAdapter) GetPodByPodUID(ctx context.Context, podUID string) (v1.Pod, error) { - ret := _mock.Called(ctx, podUID) - - if len(ret) == 0 { - panic("no return value specified for GetPodByPodUID") - } - - var r0 v1.Pod - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string) (v1.Pod, error)); ok { - return returnFunc(ctx, podUID) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, string) v1.Pod); ok { - r0 = returnFunc(ctx, podUID) - } else { - r0 = ret.Get(0).(v1.Pod) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = returnFunc(ctx, podUID) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// MockK8sAdapter_GetPodByPodUID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPodByPodUID' -type MockK8sAdapter_GetPodByPodUID_Call struct { - *mock.Call -} - -// GetPodByPodUID is a helper method to define mock.On call -// - ctx context.Context -// - podUID string -func (_e *MockK8sAdapter_Expecter) GetPodByPodUID(ctx interface{}, podUID interface{}) *MockK8sAdapter_GetPodByPodUID_Call { - return &MockK8sAdapter_GetPodByPodUID_Call{Call: _e.mock.On("GetPodByPodUID", ctx, podUID)} -} - -func (_c *MockK8sAdapter_GetPodByPodUID_Call) Run(run func(ctx context.Context, podUID string)) *MockK8sAdapter_GetPodByPodUID_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *MockK8sAdapter_GetPodByPodUID_Call) Return(pod v1.Pod, err error) *MockK8sAdapter_GetPodByPodUID_Call { - _c.Call.Return(pod, err) - return _c -} - -func (_c *MockK8sAdapter_GetPodByPodUID_Call) RunAndReturn(run func(ctx context.Context, podUID string) (v1.Pod, error)) *MockK8sAdapter_GetPodByPodUID_Call { - _c.Call.Return(run) - return _c -} diff --git a/cache/cache_test.go b/cache/cache_test.go deleted file mode 100644 index fa56ab1..0000000 --- a/cache/cache_test.go +++ /dev/null @@ -1,291 +0,0 @@ -package cache_test - -import ( - "reflect" - "testing" - "time" - - "github.com/Gthulhu/api/cache" - "github.com/Gthulhu/api/domain" - apiv1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// TestStrategyCache_ShouldReturnCachedWhenNoChanges tests that cache returns stored strategies -// when there are no pod changes and no strategy changes -func TestStrategyCache_ShouldReturnCachedWhenNoChanges(t *testing.T) { - // Arrange - cache := cache.NewStrategyCache() - initialPods := []*domain.PodInfo{ - {PodUID: "pod1", Processes: []domain.PodProcess{{PID: 100, Command: "test"}}}, - {PodUID: "pod2", Processes: []domain.PodProcess{{PID: 200, Command: "test"}}}, - } - inputStrategies := []*domain.SchedulingStrategy{ - {Priority: true, ExecutionTime: 1000, Selectors: []domain.LabelSelector{{Key: "app", Value: "test"}}}, - } - initialStrategies := []*domain.SchedulingStrategy{ - {Priority: true, ExecutionTime: 1000, PID: 100}, - {Priority: false, ExecutionTime: 2000, PID: 200}, - } - - // Act - Set up cache with initial data - cache.UpdatePodSnapshot(initialPods) - cache.UpdateStrategySnapshot(inputStrategies) - cache.SetStrategies(initialStrategies) - - // First call should return from cache (cache hit) - firstResult := cache.GetStrategies(initialPods, inputStrategies) - - // Second call with same pods and strategies should also return from cache (another cache hit) - secondResult := cache.GetStrategies(initialPods, inputStrategies) - - // Assert - if !reflect.DeepEqual(firstResult, secondResult) { - t.Error("Expected same strategies from cache when pods and strategies haven't changed") - } - - // Both calls should be cache hits since we set strategies before calling GetStrategies - if cache.GetCacheHits() != 2 { - t.Errorf("Expected 2 cache hits, got %d", cache.GetCacheHits()) - } -} - -// TestStrategyCache_ShouldInvalidateOnNewPod tests that cache invalidates when new pod is added -func TestStrategyCache_ShouldInvalidateOnNewPod(t *testing.T) { - // Arrange - cache := cache.NewStrategyCache() - initialPods := []*domain.PodInfo{ - {PodUID: "pod1", Processes: []domain.PodProcess{{PID: 100, Command: "test"}}}, - } - updatedPods := []*domain.PodInfo{ - {PodUID: "pod1", Processes: []domain.PodProcess{{PID: 100, Command: "test"}}}, - {PodUID: "pod2", Processes: []domain.PodProcess{{PID: 200, Command: "test"}}}, // New pod - } - inputStrategies := []*domain.SchedulingStrategy{ - {Priority: true, ExecutionTime: 1000, Selectors: []domain.LabelSelector{{Key: "app", Value: "test"}}}, - } - - // Act - cache.UpdatePodSnapshot(initialPods) - cache.UpdateStrategySnapshot(inputStrategies) - cache.SetStrategies([]*domain.SchedulingStrategy{{Priority: true, ExecutionTime: 1000, PID: 100}}) - _ = cache.GetStrategies(initialPods, inputStrategies) - - // Should detect change and invalidate - hasChanged := cache.HasPodsChanged(updatedPods) - - // Assert - if !hasChanged { - t.Error("Expected cache to detect new pod addition") - } - - if cache.IsValid() { - t.Error("Expected cache to be invalidated after pod change") - } -} - -// TestStrategyCache_ShouldInvalidateOnPodRestart tests cache invalidation on pod restart -func TestStrategyCache_ShouldInvalidateOnPodRestart(t *testing.T) { - // Arrange - cache := cache.NewStrategyCache() - initialPods := []*domain.PodInfo{ - {PodUID: "pod1", Processes: []domain.PodProcess{{PID: 100, Command: "test"}}}, - } - // Same pod UID but different PID (restart scenario) - restartedPods := []*domain.PodInfo{ - {PodUID: "pod1", Processes: []domain.PodProcess{{PID: 150, Command: "test"}}}, - } - inputStrategies := []*domain.SchedulingStrategy{ - {Priority: true, ExecutionTime: 1000, Selectors: []domain.LabelSelector{{Key: "app", Value: "test"}}}, - } - - // Act - cache.UpdatePodSnapshot(initialPods) - cache.UpdateStrategySnapshot(inputStrategies) - cache.SetStrategies([]*domain.SchedulingStrategy{{Priority: true, ExecutionTime: 1000, PID: 100}}) - _ = cache.GetStrategies(initialPods, inputStrategies) - - hasChanged := cache.HasPodsChanged(restartedPods) - - // Assert - if !hasChanged { - t.Error("Expected cache to detect pod restart (PID change)") - } - - if cache.IsValid() { - t.Error("Expected cache to be invalidated after pod restart") - } -} - -// TestStrategyCache_ShouldNotInvalidateOnIrrelevantChanges tests that cache stays valid -// when changes don't affect scheduling -func TestStrategyCache_ShouldNotInvalidateOnIrrelevantChanges(t *testing.T) { - // Arrange - cache := cache.NewStrategyCache() - initialPods := []*domain.PodInfo{ - {PodUID: "pod1", Processes: []domain.PodProcess{{PID: 100, Command: "test"}}}, - {PodUID: "pod2", Processes: []domain.PodProcess{{PID: 200, Command: "other"}}}, - } - // Same pods, just different order - reorderedPods := []*domain.PodInfo{ - {PodUID: "pod2", Processes: []domain.PodProcess{{PID: 200, Command: "other"}}}, - {PodUID: "pod1", Processes: []domain.PodProcess{{PID: 100, Command: "test"}}}, - } - inputStrategies := []*domain.SchedulingStrategy{ - {Priority: true, ExecutionTime: 1000, Selectors: []domain.LabelSelector{{Key: "app", Value: "test"}}}, - } - - // Act - cache.UpdatePodSnapshot(initialPods) - cache.UpdateStrategySnapshot(inputStrategies) - cache.SetStrategies([]*domain.SchedulingStrategy{{Priority: true, ExecutionTime: 1000, PID: 100}}) - _ = cache.GetStrategies(initialPods, inputStrategies) - - hasChanged := cache.HasPodsChanged(reorderedPods) - - // Assert - if hasChanged { - t.Error("Expected cache to remain valid when only order changes") - } - - if !cache.IsValid() { - t.Error("Expected cache to stay valid for irrelevant changes") - } -} - -// TestStrategyCache_ShouldExpireAfterTTL tests cache expiration -func TestStrategyCache_ShouldExpireAfterTTL(t *testing.T) { - // Arrange - cache := cache.NewStrategyCacheWithTTL(100 * time.Millisecond) // Short TTL for testing - pods := []*domain.PodInfo{ - {PodUID: "pod1", Processes: []domain.PodProcess{{PID: 100, Command: "test"}}}, - } - inputStrategies := []*domain.SchedulingStrategy{ - {Priority: true, ExecutionTime: 1000, Selectors: []domain.LabelSelector{{Key: "app", Value: "test"}}}, - } - - // Act - cache.UpdatePodSnapshot(pods) - cache.UpdateStrategySnapshot(inputStrategies) - cache.SetStrategies([]*domain.SchedulingStrategy{{Priority: true, ExecutionTime: 1000, PID: 100}}) - _ = cache.GetStrategies(pods, inputStrategies) - - // Wait for TTL to expire - time.Sleep(150 * time.Millisecond) - - // Assert - if cache.IsValid() { - t.Error("Expected cache to expire after TTL") - } -} - -// TestStrategyCache_ShouldInvalidateOnStrategyChange tests that cache invalidates when strategies change -func TestStrategyCache_ShouldInvalidateOnStrategyChange(t *testing.T) { - // Arrange - cache := cache.NewStrategyCache() - pods := []*domain.PodInfo{ - {PodUID: "pod1", Processes: []domain.PodProcess{{PID: 100, Command: "test"}}}, - } - initialStrategies := []*domain.SchedulingStrategy{ - {Priority: true, ExecutionTime: 1000, Selectors: []domain.LabelSelector{{Key: "app", Value: "test"}}}, - } - updatedStrategies := []*domain.SchedulingStrategy{ - {Priority: false, ExecutionTime: 2000, Selectors: []domain.LabelSelector{{Key: "app", Value: "prod"}}}, // Changed strategy - } - - // Act - cache.UpdatePodSnapshot(pods) - cache.UpdateStrategySnapshot(initialStrategies) - cache.SetStrategies([]*domain.SchedulingStrategy{{Priority: true, ExecutionTime: 1000, PID: 100}}) - - // First call with initial strategies - should hit cache - firstResult := cache.GetStrategies(pods, initialStrategies) - if firstResult == nil { - t.Error("Expected cache hit with initial strategies") - } - - // Second call with changed strategies - should miss cache - secondResult := cache.GetStrategies(pods, updatedStrategies) - if secondResult != nil { - t.Error("Expected cache miss when strategies changed") - } - - // Assert - if cache.GetCacheHits() != 1 { - t.Errorf("Expected 1 cache hit, got %d", cache.GetCacheHits()) - } - if cache.GetCacheMisses() != 1 { - t.Errorf("Expected 1 cache miss, got %d", cache.GetCacheMisses()) - } -} - -// TestPodChangeDetector_ComputeFingerprint tests pod fingerprint computation -func TestPodChangeDetector_ComputeFingerprint(t *testing.T) { - // Arrange - detector := cache.NewPodChangeDetector() - pods1 := []*domain.PodInfo{ - {PodUID: "pod1", Processes: []domain.PodProcess{{PID: 100, Command: "test"}}}, - } - pods2 := []*domain.PodInfo{ - {PodUID: "pod1", Processes: []domain.PodProcess{{PID: 100, Command: "test"}}}, - } - pods3 := []*domain.PodInfo{ - {PodUID: "pod1", Processes: []domain.PodProcess{{PID: 200, Command: "test"}}}, // Different PID - } - - // Act - fingerprint1 := detector.ComputeFingerprint(pods1) - fingerprint2 := detector.ComputeFingerprint(pods2) - fingerprint3 := detector.ComputeFingerprint(pods3) - - // Assert - if fingerprint1 != fingerprint2 { - t.Error("Expected same fingerprint for identical pod states") - } - - if fingerprint1 == fingerprint3 { - t.Error("Expected different fingerprint for different PIDs") - } -} - -// TestKubernetesPodWatcher_ShouldDetectPodEvents tests Kubernetes pod watcher -func TestKubernetesPodWatcher_ShouldDetectPodEvents(t *testing.T) { - // This test would require mock Kubernetes client - // For now, we'll define the interface - - // Arrange - watcher := cache.NewPodWatcher() - changeDetected := false - - // Register callback for pod changes - watcher.OnPodChange(func() { - changeDetected = true - }) - - // Act - Simulate pod event - watcher.SimulateEvent(cache.PodEvent{ - Type: "ADDED", - Pod: apiv1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - UID: "new-pod", - }, - }, - }) - - // Assert - if !changeDetected { - t.Error("Expected watcher to detect pod addition event") - } -} - -// TestIntegration_CacheWithRealAPI tests the complete flow -func TestIntegration_CacheWithRealAPI(t *testing.T) { - // This would be an integration test combining all components - t.Run("Should use cache when no changes", func(t *testing.T) { - // Test the full flow with cache - }) - - t.Run("Should recalculate on pod changes", func(t *testing.T) { - // Test cache invalidation and recalculation - }) -} diff --git a/cache/pod_watcher.go b/cache/pod_watcher.go deleted file mode 100644 index 535a923..0000000 --- a/cache/pod_watcher.go +++ /dev/null @@ -1,114 +0,0 @@ -package cache - -import ( - "sync" - "time" - - "github.com/Gthulhu/api/util" - apiv1 "k8s.io/api/core/v1" - "k8s.io/client-go/informers" - "k8s.io/client-go/kubernetes" - kcache "k8s.io/client-go/tools/cache" -) - -var ( - // Define Pod label cache to reduce API call frequency - podLabelCache = make(map[string]apiv1.Pod) - podLabelCacheMu sync.RWMutex - podLabelCacheTTL = 30 * time.Second - podLabelCacheTime = make(map[string]time.Time) -) - -// StartPodWatcher starts watching Kubernetes pod events and invalidates cache on changes -func StartPodWatcher(cache *StrategyCache, kubeClient *kubernetes.Clientset) (stopCh chan struct{}, err error) { - client := kubeClient - stopCh = make(chan struct{}) - - // Start watching pods in all namespaces using SharedInformer - go func() { - // Shared informer factory across all namespaces; 0 disables periodic resync - factory := informers.NewSharedInformerFactory(client, 0) - podInformer := factory.Core().V1().Pods().Informer() - - // Register event handlers - podInformer.AddEventHandler(kcache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { - if pod, ok := obj.(*apiv1.Pod); ok { - // Update label cache - podLabelCacheMu.Lock() - podLabelCache[string(pod.UID)] = *pod - podLabelCacheTime[string(pod.UID)] = time.Now() - podLabelCacheMu.Unlock() - } - cache.Invalidate() - util.GetLogger().Info("Pod Added event: cache invalidated") - }, - UpdateFunc: func(oldObj, newObj interface{}) { - if pod, ok := newObj.(*apiv1.Pod); ok { - podLabelCacheMu.Lock() - podLabelCache[string(pod.UID)] = *pod - podLabelCacheTime[string(pod.UID)] = time.Now() - podLabelCacheMu.Unlock() - } - cache.Invalidate() - util.GetLogger().Info("Pod Updated event: cache invalidated") - }, - DeleteFunc: func(obj interface{}) { - switch t := obj.(type) { - case *apiv1.Pod: - podLabelCacheMu.Lock() - delete(podLabelCache, string(t.UID)) - delete(podLabelCacheTime, string(t.UID)) - podLabelCacheMu.Unlock() - case kcache.DeletedFinalStateUnknown: - if pod, ok := t.Obj.(*apiv1.Pod); ok { - podLabelCacheMu.Lock() - delete(podLabelCache, string(pod.UID)) - delete(podLabelCacheTime, string(pod.UID)) - podLabelCacheMu.Unlock() - } - } - cache.Invalidate() - util.GetLogger().Info("Pod Deleted event: cache invalidated") - }, - }) - - factory.Start(stopCh) - - // Wait for caches to sync and then keep running; this will handle reconnects internally - if ok := kcache.WaitForCacheSync(stopCh, podInformer.HasSynced); !ok { - util.GetLogger().Warn("Pod informer cache sync failed; will continue to retry via client-go mechanisms") - } else { - util.GetLogger().Info("Pod informer started successfully") - } - - // Block forever; use stopCh to stop if we add stop semantics later - <-stopCh - }() - - return stopCh, nil -} - -// GetKubernetesPod retrieves pod information from the cache if available -func GetKubernetesPod(podUID string) (apiv1.Pod, bool) { - // Check cache - podLabelCacheMu.RLock() - cachedLabels, exists := podLabelCache[podUID] - cacheTime, timeExists := podLabelCacheTime[podUID] - podLabelCacheMu.RUnlock() - - // If the cache exists and is not expired, return it directly - if exists && timeExists && time.Since(cacheTime) < podLabelCacheTTL { - return cachedLabels, true - } - - return apiv1.Pod{}, false -} - -// SetKubernetesPodCache sets the pod information in the cache -func SetKubernetesPodCache(podUID string, pod apiv1.Pod) { - podLabelCacheMu.Lock() - podLabelCache[podUID] = pod - podLabelCacheTime[podUID] = time.Now() - podLabelCacheMu.Unlock() -} diff --git a/cache/strategy_cache.go b/cache/strategy_cache.go deleted file mode 100644 index 8bb035d..0000000 --- a/cache/strategy_cache.go +++ /dev/null @@ -1,457 +0,0 @@ -package cache - -import ( - "crypto/sha256" - "encoding/json" - "fmt" - "sort" - "sync" - "time" - - "github.com/Gthulhu/api/domain" - apiv1 "k8s.io/api/core/v1" -) - -// StrategyCache manages caching of scheduling strategies -type StrategyCache struct { - mu sync.RWMutex - cachedStrategies []*domain.SchedulingStrategy - podFingerprint string - strategyFingerprint string - lastUpdate time.Time - ttl time.Duration - valid bool - cacheHits int - cacheMisses int -} - -// NewStrategyCache creates a new strategy cache with default TTL -func NewStrategyCache() *StrategyCache { - return &StrategyCache{ - ttl: 5 * time.Minute, // Default TTL - valid: false, - } -} - -// NewStrategyCacheWithTTL creates a cache with custom TTL -func NewStrategyCacheWithTTL(ttl time.Duration) *StrategyCache { - return &StrategyCache{ - ttl: ttl, - valid: false, - } -} - -// UpdatePodSnapshot updates the pod fingerprint -func (c *StrategyCache) UpdatePodSnapshot(pods []*domain.PodInfo) { - c.mu.Lock() - defer c.mu.Unlock() - - detector := NewPodChangeDetector() - c.podFingerprint = detector.ComputeFingerprint(pods) -} - -// UpdateStrategySnapshot updates the strategy fingerprint -func (c *StrategyCache) UpdateStrategySnapshot(strategies []*domain.SchedulingStrategy) { - c.mu.Lock() - defer c.mu.Unlock() - - c.strategyFingerprint = ComputeStrategyFingerprint(strategies) -} - -// SetStrategies stores strategies in cache -func (c *StrategyCache) SetStrategies(strategies []*domain.SchedulingStrategy) { - c.mu.Lock() - defer c.mu.Unlock() - c.cachedStrategies = strategies - c.lastUpdate = time.Now() - c.valid = true -} - -// GetStrategiesQuick returns cached strategies without checking pod state -// Relies on Kubernetes Watch to invalidate cache when pods change -func (c *StrategyCache) GetStrategiesQuick(inputStrategies []*domain.SchedulingStrategy) []*domain.SchedulingStrategy { - c.mu.RLock() - - // Quick validation checks - cacheValid := c.valid && len(c.cachedStrategies) > 0 - if cacheValid && time.Since(c.lastUpdate) > c.ttl { - cacheValid = false - } - - // Check strategy fingerprint - if cacheValid { - currentStrategyFingerprint := ComputeStrategyFingerprint(inputStrategies) - if currentStrategyFingerprint != c.strategyFingerprint { - cacheValid = false - } - } - - // Return cached copy if valid - if cacheValid { - cachedStrategies := make([]*domain.SchedulingStrategy, len(c.cachedStrategies)) - copy(cachedStrategies, c.cachedStrategies) - c.mu.RUnlock() - - // Update hit counter - c.mu.Lock() - c.cacheHits++ - c.mu.Unlock() - - return cachedStrategies - } - - c.mu.RUnlock() - - // Cache miss - c.mu.Lock() - c.cacheMisses++ - c.mu.Unlock() - - return nil -} - -// GetStrategies returns cached strategies if valid, otherwise returns nil -// This version still checks pod fingerprint for backward compatibility -func (c *StrategyCache) GetStrategies(currentPods []*domain.PodInfo, inputStrategies []*domain.SchedulingStrategy) []*domain.SchedulingStrategy { - // First, do a quick read-only check - c.mu.RLock() - cacheValid := c.valid && len(c.cachedStrategies) > 0 - if cacheValid { - // Check if cache is expired - if time.Since(c.lastUpdate) > c.ttl { - cacheValid = false - } - } - - // If valid, check pod fingerprint - var currentPodFingerprint string - if cacheValid { - detector := NewPodChangeDetector() - currentPodFingerprint = detector.ComputeFingerprint(currentPods) - if currentPodFingerprint != c.podFingerprint { - cacheValid = false - } - } - - // If still valid, check strategy fingerprint - var currentStrategyFingerprint string - if cacheValid { - currentStrategyFingerprint = ComputeStrategyFingerprint(inputStrategies) - if currentStrategyFingerprint != c.strategyFingerprint { - cacheValid = false - } - } - - // If still valid, return cached copy - if cacheValid { - cachedStrategies := make([]*domain.SchedulingStrategy, len(c.cachedStrategies)) - copy(cachedStrategies, c.cachedStrategies) - c.mu.RUnlock() - - // Update hit counter with separate lock - c.mu.Lock() - c.cacheHits++ - c.mu.Unlock() - - return cachedStrategies - } - - // Cache miss - release read lock and acquire write lock - c.mu.RUnlock() - - c.mu.Lock() - defer c.mu.Unlock() - - // Double-check validity after acquiring write lock - // (another goroutine might have updated cache) - if c.valid && len(c.cachedStrategies) > 0 { - if time.Since(c.lastUpdate) <= c.ttl { - detector := NewPodChangeDetector() - currentPodFingerprint = detector.ComputeFingerprint(currentPods) - currentStrategyFingerprint = ComputeStrategyFingerprint(inputStrategies) - if currentPodFingerprint == c.podFingerprint && currentStrategyFingerprint == c.strategyFingerprint { - // Cache became valid while we were waiting for lock - cachedStrategies := make([]*domain.SchedulingStrategy, len(c.cachedStrategies)) - copy(cachedStrategies, c.cachedStrategies) - c.cacheHits++ - return cachedStrategies - } - } - } - - // Definitely a miss - c.valid = false - c.cacheMisses++ - return nil -} - -// HasPodsChanged checks if pods have changed since last snapshot -func (c *StrategyCache) HasPodsChanged(pods []*domain.PodInfo) bool { - c.mu.RLock() - detector := NewPodChangeDetector() - currentFingerprint := detector.ComputeFingerprint(pods) - lastFingerprint := c.podFingerprint - c.mu.RUnlock() - - changed := currentFingerprint != lastFingerprint - - if changed { - // Invalidate cache if pods have changed - c.mu.Lock() - c.valid = false - c.mu.Unlock() - } - - return changed -} - -// IsValid returns whether cache is valid -func (c *StrategyCache) IsValid() bool { - c.mu.RLock() - defer c.mu.RUnlock() - - // Check TTL - if time.Since(c.lastUpdate) > c.ttl { - return false - } - - return c.valid -} - -// Invalidate marks cache as invalid -func (c *StrategyCache) Invalidate() { - c.mu.Lock() - defer c.mu.Unlock() - c.valid = false -} - -// GetCacheHits returns number of cache hits -func (c *StrategyCache) GetCacheHits() int { - c.mu.RLock() - defer c.mu.RUnlock() - return c.cacheHits -} - -// GetCacheMisses returns number of cache misses -func (c *StrategyCache) GetCacheMisses() int { - c.mu.RLock() - defer c.mu.RUnlock() - return c.cacheMisses -} - -// GetStats returns cache statistics -func (c *StrategyCache) GetStats() map[string]interface{} { - c.mu.RLock() - defer c.mu.RUnlock() - - hitRate := float64(0) - if c.cacheHits+c.cacheMisses > 0 { - hitRate = float64(c.cacheHits) / float64(c.cacheHits+c.cacheMisses) * 100 - } - - return map[string]interface{}{ - "hits": c.cacheHits, - "misses": c.cacheMisses, - "hit_rate": fmt.Sprintf("%.2f%%", hitRate), - "valid": c.valid, - "last_update": c.lastUpdate.Format(time.RFC3339), - "ttl_seconds": c.ttl.Seconds(), - } -} - -// PodChangeDetector computes fingerprints for pod states -type PodChangeDetector struct{} - -// NewPodChangeDetector creates a new pod change detector -func NewPodChangeDetector() *PodChangeDetector { - return &PodChangeDetector{} -} - -// PodFingerprint represents essential pod information for change detection -type PodFingerprint struct { - UID string - Processes []ProcessFingerprint -} - -// ProcessFingerprint represents essential process information -type ProcessFingerprint struct { - PID int - Command string -} - -// ComputeFingerprint generates a unique fingerprint for pod state -func (d *PodChangeDetector) ComputeFingerprint(pods []*domain.PodInfo) string { - // Create a deterministic representation - fingerprints := make([]PodFingerprint, len(pods)) - - for i, pod := range pods { - processes := make([]ProcessFingerprint, len(pod.Processes)) - for j, proc := range pod.Processes { - processes[j] = ProcessFingerprint{ - PID: proc.PID, - Command: proc.Command, - } - } - - // Sort processes by PID for consistency - sort.Slice(processes, func(i, j int) bool { - return processes[i].PID < processes[j].PID - }) - - fingerprints[i] = PodFingerprint{ - UID: pod.PodUID, - Processes: processes, - } - } - - // Sort pods by UID for consistency - sort.Slice(fingerprints, func(i, j int) bool { - return fingerprints[i].UID < fingerprints[j].UID - }) - - // Compute hash - data, _ := json.Marshal(fingerprints) - hash := sha256.Sum256(data) - return fmt.Sprintf("%x", hash) -} - -// ComputeStrategyFingerprint generates a unique fingerprint for scheduling strategies -// This excludes PID field as PIDs are calculated, not part of input strategy -func ComputeStrategyFingerprint(strategies []*domain.SchedulingStrategy) string { - // Create a deterministic representation excluding calculated PIDs - type StrategyKey struct { - Priority bool - ExecutionTime uint64 - Selectors []domain.LabelSelector - CommandRegex string - } - - keys := make([]StrategyKey, len(strategies)) - for i, s := range strategies { - // Sort selectors for consistency - selectors := make([]domain.LabelSelector, len(s.Selectors)) - copy(selectors, s.Selectors) - sort.Slice(selectors, func(i, j int) bool { - if selectors[i].Key != selectors[j].Key { - return selectors[i].Key < selectors[j].Key - } - return selectors[i].Value < selectors[j].Value - }) - - keys[i] = StrategyKey{ - Priority: s.Priority, - ExecutionTime: s.ExecutionTime, - Selectors: selectors, - CommandRegex: s.CommandRegex, - } - } - - // Sort strategies for consistency - sort.Slice(keys, func(i, j int) bool { - // First by priority - if keys[i].Priority != keys[j].Priority { - return keys[i].Priority - } - // Then by execution time - if keys[i].ExecutionTime != keys[j].ExecutionTime { - return keys[i].ExecutionTime < keys[j].ExecutionTime - } - // Then by command regex - return keys[i].CommandRegex < keys[j].CommandRegex - }) - - // Compute hash - data, _ := json.Marshal(keys) - hash := sha256.Sum256(data) - return fmt.Sprintf("%x", hash) -} - -// PodEvent represents a Kubernetes pod event -type PodEvent struct { - Type string - Pod apiv1.Pod -} - -// PodWatcher watches for Kubernetes pod changes -type PodWatcher struct { - mu sync.RWMutex - changeCallbacks []func() - stopChan chan struct{} - running bool -} - -// NewPodWatcher creates a new pod watcher -func NewPodWatcher() *PodWatcher { - return &PodWatcher{ - changeCallbacks: make([]func(), 0), - stopChan: make(chan struct{}), - } -} - -// OnPodChange registers a callback for pod changes -func (w *PodWatcher) OnPodChange(callback func()) { - w.mu.Lock() - defer w.mu.Unlock() - w.changeCallbacks = append(w.changeCallbacks, callback) -} - -// SimulateEvent simulates a pod event (for testing) -func (w *PodWatcher) SimulateEvent(event PodEvent) { - w.notifyCallbacks() -} - -// notifyCallbacks calls all registered callbacks -func (w *PodWatcher) notifyCallbacks() { - w.mu.RLock() - callbacks := make([]func(), len(w.changeCallbacks)) - copy(callbacks, w.changeCallbacks) - w.mu.RUnlock() - - for _, callback := range callbacks { - callback() - } -} - -// Start begins watching for pod changes -func (w *PodWatcher) Start() error { - w.mu.Lock() - defer w.mu.Unlock() - - if w.running { - return fmt.Errorf("watcher already running") - } - - w.running = true - - // In production, this would use Kubernetes watch API - go w.watchLoop() - - return nil -} - -// Stop stops the watcher -func (w *PodWatcher) Stop() { - w.mu.Lock() - defer w.mu.Unlock() - - if w.running { - close(w.stopChan) - w.running = false - } -} - -// watchLoop is the main watch loop -func (w *PodWatcher) watchLoop() { - // In production, this would set up Kubernetes watch - for { - select { - case <-w.stopChan: - return - default: - // Would process Kubernetes events here - } - } -} - -// Global cache instance -var strategyCache = NewStrategyCache() diff --git a/config/config.go b/config/config.go deleted file mode 100644 index ffd129d..0000000 --- a/config/config.go +++ /dev/null @@ -1,188 +0,0 @@ -package config - -import ( - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/json" - "encoding/pem" - "fmt" - "log/slog" - "os" - "strings" - - "github.com/Gthulhu/api/util" -) - -// Config represents the application configuration -type Config struct { - Server ServerConfig `json:"server"` - Logging LoggingConfig `json:"logging"` - Strategies StrategiesConfig `json:"strategies"` - JWT JWTConfig `json:"jwt"` -} - -// ServerConfig represents server-specific configuration -type ServerConfig struct { - Port string `json:"port"` - ReadTimeout int `json:"read_timeout"` - WriteTimeout int `json:"write_timeout"` - IdleTimeout int `json:"idle_timeout"` -} - -// LoggingConfig represents logging configuration -type LoggingConfig struct { - Level string `json:"level"` - Format string `json:"format"` -} - -// SchedulingStrategy represents a strategy for process scheduling -type SchedulingStrategy struct { - Priority bool `json:"priority"` // If true, set vtime to minimum vtime - ExecutionTime uint64 `json:"execution_time"` // Time slice for this process in nanoseconds - PID int `json:"pid,omitempty"` // Process ID to apply this strategy to - Selectors []LabelSelector `json:"selectors,omitempty"` // Label selectors to match pods - CommandRegex string `json:"command_regex,omitempty"` // Regex to match process command -} - -// LabelSelector represents a key-value pair for pod label selection -type LabelSelector struct { - Key string `json:"key"` // Label key - Value string `json:"value"` // Label value -} - -// StrategiesConfig represents scheduling strategies configuration -type StrategiesConfig struct { - Default []SchedulingStrategy `json:"default"` -} - -// JWTConfig represents JWT authentication configuration -type JWTConfig struct { - PrivateKeyPath string `json:"private_key_path"` - TokenDuration int `json:"token_duration"` // Token duration in hours -} - -// LoadConfig loads configuration from file or returns default config -func LoadConfig(filename string) (*Config, error) { - // Default configuration - config := &Config{ - Server: ServerConfig{ - Port: ":8080", - ReadTimeout: 15, - WriteTimeout: 15, - IdleTimeout: 60, - }, - Logging: LoggingConfig{ - Level: "info", - Format: "text", - }, - Strategies: StrategiesConfig{ - Default: []SchedulingStrategy{ - { - Priority: true, - ExecutionTime: 20000000, - Selectors: []LabelSelector{ - { - Key: "nf", - Value: "upf", - }, - }, - }, - }, - }, - JWT: JWTConfig{ - PrivateKeyPath: "/etc/bss-api/private_key.pem", - TokenDuration: 24, // 24 hours - }, - } - - // Try to load from file - if filename != "" { - file, err := os.Open(filename) - if err != nil { - return config, nil // Return default config if file doesn't exist - } - defer file.Close() - - decoder := json.NewDecoder(file) - if err := decoder.Decode(config); err != nil { - return nil, fmt.Errorf("failed to decode config file: %v", err) - } - } - - return config, nil -} - -// InitJWTRsaKey initializes RSA private key for JWT authentication -func InitJWTRsaKey(config JWTConfig) (*rsa.PrivateKey, error) { - - // Try to load existing private key - key, err := loadPrivateKey(config.PrivateKeyPath) - if err != nil { - util.GetLogger().Warn("Failed to load private key, generating a new one", slog.String("path", config.PrivateKeyPath), util.LogErrAttr(err)) - // Generate new private key - key, err = generatePrivateKey(config.PrivateKeyPath) - if err != nil { - return nil, fmt.Errorf("failed to generate private key: %v", err) - } - } - - return key, nil -} - -// loadPrivateKey loads RSA private key from PEM file -func loadPrivateKey(keyPath string) (*rsa.PrivateKey, error) { - keyData, err := os.ReadFile(keyPath) - if err != nil { - return nil, fmt.Errorf("failed to read private key file: %v", err) - } - - block, _ := pem.Decode(keyData) - if block == nil { - return nil, fmt.Errorf("failed to decode PEM block containing private key") - } - - key, err := x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - // Try PKCS8 format - keyInterface, err := x509.ParsePKCS8PrivateKey(block.Bytes) - if err != nil { - return nil, fmt.Errorf("failed to parse private key: %v", err) - } - var ok bool - key, ok = keyInterface.(*rsa.PrivateKey) - if !ok { - return nil, fmt.Errorf("private key is not RSA") - } - } - - return key, nil -} - -// generatePrivateKey generates a new RSA private key and saves it to file -func generatePrivateKey(keyPath string) (*rsa.PrivateKey, error) { - // Generate RSA private key - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, fmt.Errorf("failed to generate private key: %v", err) - } - - // Create directory if it doesn't exist - if err := os.MkdirAll(strings.TrimSuffix(keyPath, "/private_key.pem"), 0755); err != nil { - util.GetLogger().Warn("Failed to create directory for private key", slog.String("path", keyPath), util.LogErrAttr(err)) - } - - // Save private key to file - keyBytes := x509.MarshalPKCS1PrivateKey(privateKey) - keyPEM := pem.EncodeToMemory(&pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: keyBytes, - }) - - if err := os.WriteFile(keyPath, keyPEM, 0600); err != nil { - util.GetLogger().Warn("Failed to save private key to file", slog.String("path", keyPath), util.LogErrAttr(err)) - } else { - util.GetLogger().Info("Generated and saved new private key", slog.String("path", keyPath)) - } - return privateKey, nil -} diff --git a/config/config.json b/config/config.json deleted file mode 100644 index 3ebc694..0000000 --- a/config/config.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "server": { - "port": ":8080", - "read_timeout": 15, - "write_timeout": 15, - "idle_timeout": 60 - }, - "logging": { - "level": "info", - "format": "text" - }, - "jwt": { - "private_key_path": "./config/jwt_private_key.key", - "token_duration": 24 - }, - "strategies": { - "default": [ - { - "priority": true, - "execution_time": 20000, - "selectors": [ - { - "key": "app", - "value": "ueransim-macvlan" - } - ], - "command_regex": "nr-gnb|nr-ue|ping" - } - ] - } -} diff --git a/config/manager_config.default.toml b/config/manager_config.default.toml new file mode 100644 index 0000000..8d227d5 --- /dev/null +++ b/config/manager_config.default.toml @@ -0,0 +1,33 @@ +[server] +host = ":8080" + + +[logging] +level = "info" +path = "logs/app.log" + +[mongodb] +uri = "mongodb://test:test@localhost:27017/?tls=false" +database = "appdb" +ca_pem = """ +-----BEGIN CERTIFICATE----- +YOUR_CERTIFICATE_HERE +-----END CERTIFICATE----- +""" + +[key] +rsa_private_key_pem = """ +-----BEGIN RSA PRIVATE KEY----- +YOUR_PRIVATE_KEY_HERE +-----END RSA PRIVATE KEY----- +""" + +rsa_public_key_pem = """ +-----BEGIN PUBLIC KEY----- +YOUR_PUBLIC_KEY_HERE +-----END PUBLIC KEY----- +""" + +[account] +admin_email = "admin@example.com" +admin_password = "your-password-here" diff --git a/config/manager_config.go b/config/manager_config.go new file mode 100644 index 0000000..8f463ed --- /dev/null +++ b/config/manager_config.go @@ -0,0 +1,85 @@ +package config + +import ( + "path/filepath" + "runtime" + "strings" + + "github.com/spf13/viper" +) + +type ServerConfig struct { + Host string `mapstructure:"host"` +} + +type LoggingConfig struct { + Level string `mapstructure:"level"` + Console bool `mapstructure:"console"` + FilePath string `mapstructure:"file_path"` +} + +type ManageConfig struct { + Server ServerConfig `mapstructure:"server"` + Logging LoggingConfig `mapstructure:"logging"` + MongoDB MongoDBConfig `mapstructure:"mongodb"` + Key KeyConfig `mapstructure:"key"` + Account AccountConfig `mapstructure:"account"` +} + +type MongoDBConfig struct { + URI string `mapstructure:"uri"` + Database string `mapstructure:"database"` + CAPem string `mapstructure:"ca_pem"` +} + +type KeyConfig struct { + RsaPrivateKeyPem string `mapstructure:"rsa_private_key_pem"` +} + +type AccountConfig struct { + AdminEmail string `mapstructure:"admin_email"` + AdminPassword string `mapstructure:"admin_password"` +} + +var ( + managerCfg *ManageConfig +) + +func GetConfig() *ManageConfig { + return managerCfg +} + +func InitManagerConfig(configName string, configPath string) (ManageConfig, error) { + var cfg ManageConfig + if configPath != "" { + viper.AddConfigPath(configPath) + } + if configName == "" { + configName = "manager_config" + } + viper.AddConfigPath(GetAbsPath("config")) + viper.SetConfigName(configName) + viper.SetConfigType("toml") + viper.SetEnvPrefix("MANAGER") + viper.AutomaticEnv() + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + err := viper.ReadInConfig() + if err != nil { + return cfg, err + } + + err = viper.Unmarshal(&cfg) + if err != nil { + return cfg, err + } + managerCfg = &cfg + return cfg, nil +} + +// GetAbsPath returns the absolute path by joining the given paths with the project root directory +func GetAbsPath(paths ...string) string { + _, filePath, _, _ := runtime.Caller(1) + basePath := filepath.Dir(filePath) + rootPath := filepath.Join(basePath, "..") + return filepath.Join(rootPath, filepath.Join(paths...)) +} diff --git a/config/manager_config.test.toml b/config/manager_config.test.toml new file mode 100644 index 0000000..442ff83 --- /dev/null +++ b/config/manager_config.test.toml @@ -0,0 +1,71 @@ +[server] +host = ":8080" + + +[logging] + +level = "info" +path = "logs/app.log" + +[mongodb] +uri = "mongodb://test:test@localhost:27017/?tls=false" +database = "appdb" + +[key] +rsa_private_key_pem = """ +-----BEGIN RSA PRIVATE KEY----- +MIIJJwIBAAKCAgEAny28YMC2/+yYj3T29lz60uryNz8gNVrqD7lTJuHQ3DMTE6AD +qnERy8VgHve0tWzhJc5ZBZ1Hduvj+z/kNqbcU81YGhmfOrQ3iFNYBlSAseIHdAw3 +9HGyC6OKzTXI4HRpc8CwcF6hKExkyWlkALr5i+IQDfimvarjjZ6Nm368L0Rthv3K +OkI5CqRZ6bsVwwBug7GcdkvFs3LiRSKlMBpH2tCkZ5ZZE8VyuK7VnlwV7n6EHzN5 +BqaHq8HVLw2KzvibSi+/5wIZV2Yx33tViLbhOsZqLt6qQCGGgKzNX4TGwRLGAiVV +1NCpgQhimZ4YP2thqSsqbaISOuvFlYq+QGP1bcvcHB7UhT1ZnHSDYcbT2qiD3Voq +ytXVKLB1X5XCD99YLSP9B32f1lvZD4MhDtE4IhAuqn15MGB5ct4yj/uMldFScs9K +hqnWcwS4K6Qx3IfdB+ZxT5hEOWJLEcGqe/CSXITNG7oS9mrSAJJvHSLz++4R/Sh1 +MnT2YWjyDk6qeeqAwut0w5iDKWt7qsGEcHFPIVVlos+xLfrPDtgHQk8upjslUcMy +MDTf21Y3RdJ3k1gTR9KHEwzKeiNlLjen9ekFWupF8jik1aYRWL6h54ZyGxwKEyMY +i9o18G2pXPzvVaPYtU+TGXdO4QwiES72TNCDbNaGj75Gj0sN+LfjjQ4A898CAwEA +AQKCAgAFrHuqdzQOq0BE3MZwwZ+vJPC9R2K+hB8TsGdmW2Y2cxua93kp+h3IRaDH +eczXKqpbzp8dtB13/7CApCZeTFROKGObio5CaWoRUecxUpHDxWq+mDDmZacTAyFP +bztZxMx9c8DWQIk+BnsRMtB9tixu7//if5px6EV0JtKlWD8c8DN3PFSY/wNJfdI2 +opSD/t/xkcMh9FF3tACctj9tF4K4KfeyOYmzSrZsHs8+dcnSVnAfLJaDxivP03jl +1HW+Kt5eJpWQhmKg2uOsM5k45kvg7HGcehNXddp1e7NWVEVBXInySaJlk4p3LvVU +xG3Y1NsGTKOWhNBhiUXhrrBZWzbERLvE7/OtrHVDAlEuA47rFb/rTnMWHIZhNeXt +Hwa7G/11dlwYN3jn5u+/2SkVC0R4X/lqTzFRzCYIWr9lTeVpC53Gn1jmX2PSNDbF +yLi7ZZFBhS8GdNimeKyJReKV2o2nsO49KngGIs5/B318GA5BwBVPf9fVd9a2n29s +ioUXmTE07bpbyXk1BL4jZFOAJYqXh6IOwAFKFtQXgfidod7KcwpxQAJpE1Od3EPd +sGlTTC+hsUzAdlV82wqc6AB80DcxlzsI0adTkD7NrIcRSQtCC9DnZPXm/kFiam80 +gW2SmIsaLYauoQgIcbI1Lpy3rMCMbbTeG7K3KGyYZULFXHRsAQKCAQEAy5n5Xt8z +VaCS+V0VrwpIdPAOzgaxJkcfy3hVLe5vkhaeu5DzyHgW0fPud0P2J1eUD8GWxXvj +6EImFsFydH+DR8ClhHX8awpEn4mS6vR6VVPseqKWzs6BP7usH/WNTIeGJR2z0hRG +ZgD/z4W1PwwL6tIno/oHY3MkflX5/1X6q7vGjNjN7d81Mb48dXcLEjaZWtcRqzy0 +MNXrgvrpe/pCRUeUBV6WYHSPn6OPJA7RgcKegn9AwWEoecieFnjiVSjdfI2WT+Wa +13MCwPoWs1u+mHNl+yaqFnj9u9tF2xF6M8ZERscmvU+k0aDNcuJq/drP6qH/93gn +e7Tjwlf3IqhcLwKCAQEAyCUFaRimDfn43qLBDZpgmBHwCpBQFaNS8jvVQpUlQ4zL +W/pCqMIePajE5E9EBcQZyd+tE6WE202Z2CBzvfyH54HGmAiEE5koQSE6GBZbXrzu +4ToolPR6nrlEDR6ayjuv9BOPR91OZL9EfSgi5kdNIEilnDm1n/VhEIW5y6TsJUGv +VeuTDgnRYbkIVBBppA5U4rYyOz7ES+0L344k05i9LzFvcgc1QX50Dg8dqiI2tm6z +uyjxhJ9TW6R1iqzLnDB01YrDuWN62qISHnNz4X5Z/EBoITo3Og2IMaCeE5yKHCjw +FrrV05F8Cf7B5DLBOTWiX3oPtu9oV7QrjN6WARGHUQKCAQARrH8KLkPthe/cN6lf +NXxOslwGpGwST5BCAGMchpsmylHjJFUVLN+GQC+OKNcgWSjgKUTmRbfl/IAD76z4 +0ezaeK2ljvxnak/ErZOUU76e05cumhiPQTvVBXyOlak7YHRTmn12mg32YtXR9OBj +5a7PJokMYfLsPh2H3fzCnnsRF07IATX3FS4v8DydUcUjQpwTV6IQBEf8CUXVa+SC +v5mrG+iMgsZ4/wVMrU0Kq0KiiftqhpNfdgimcbTPbJTxIYgAfOX0b5D+bNxrVgpM +bYVhBHtwzs1q//u+p+0rdBvwjKB2qGkDe/tpuxS6iU8SVEFCM+fdWo/K3Ev9Hde1 +KXo/AoIBABUAdYHiuUIMMgZCs9lWkr5CW5rwK8cpfUG375fuCJv/ATPknewRepTj +yc1fV/b27fHWC9Zc7wUILpWUSjDsd+JeJtW7Rwi7cJLtBqiSaAIX90UhEjMXOGrB +bBeoV3vTKZKGHunemiROQcSUWp0pbDlwBhjPoXRojkfqkGWDJ9h8/QYaEzNM6nDD +ttEDa+JwMo4bqke3PWfuNum9g7XEeE2kdVpU0UzPFSSIh4db0bvw/+Eq2bUd9uRN +7JuhqDf6ibgCuKkSfEjG6vnRCZ7m4FBs/cBG2Ja55sm2XgAW1BNCZHcuIdPylz6B +Qh1NCiOTsjcsmsuKcbuKR2ufy8PO8BECggEACr4AaasrHcqqBcGCZfGlO4Q0Bbzu +1IJ6u9X+0U+k36wH9cE8RMJZaFo30bYTSBnxN6YY4M1taG3egIZQNgTRpjEiC8lV +spxkAVwlY6g1ZER7IFXlzhz6wYuDBayRhA/2zBPgtGesfpTd7H24AlJ6qB+mThHs +7ZbETM4KP04uNBBPybwaPIOkUPnQInfIPOAJVHebHp8atyavwrpGmRq958XKMgvz +8oHIdzV+XTMi1+U7eg/ITEpOAPD82fYB4UfKRdA1jraXsJJyTs+QFjc2WXBffgAg +X6m7Mp9nAMhRyXhULslO3trWFbFCa2dbQkDSyBRvsb2HZtztoLVyo1mtUg== +-----END RSA PRIVATE KEY----- +""" + +[account] +admin_email = "admin@example.com" +admin_password = "your-password-here" diff --git a/k8s/deployment.yaml b/deployment/k8s/deployment.yaml similarity index 100% rename from k8s/deployment.yaml rename to deployment/k8s/deployment.yaml diff --git a/deployment/local/docker-compose.infra.yaml b/deployment/local/docker-compose.infra.yaml new file mode 100644 index 0000000..c786314 --- /dev/null +++ b/deployment/local/docker-compose.infra.yaml @@ -0,0 +1,13 @@ +services: + mongo: + container_name: mongo + image: mongo:4.4.27 + environment: + MONGODB_CLIENT_EXTRA_FLAGS: "--authenticationDatabase admin" + MONGO_INITDB_ROOT_USERNAME: test + MONGO_INITDB_ROOT_PASSWORD: test + ports: + - 27017:27017 +networks: + mysql_cluster: + driver: bridge diff --git a/domain/interface.go b/domain/interface.go deleted file mode 100644 index 5f91c3e..0000000 --- a/domain/interface.go +++ /dev/null @@ -1,23 +0,0 @@ -package domain - -import ( - "context" -) - -// Service defines the interface for the service layer -type Service interface { - // VerifyAndGenerateToken verifies the provided public key and generates a JWT token if valid - VerifyAndGenerateToken(ctx context.Context, publicKey string) (string, error) - // GetAllPodInfos retrieves all pod information by scanning the /proc filesystem - GetAllPodInfos(ctx context.Context) ([]*PodInfo, error) - // SaveBSSMetrics saves the provided BSS metrics data - SaveBSSMetrics(ctx context.Context, bssMetrics *BssData) error - // GetBSSMetrics retrieves the latest BSS metrics data - GetBSSMetrics(ctx context.Context) (*BssData, error) - // SaveSchedulingStrategy saves the provided scheduling strategies - SaveSchedulingStrategy(ctx context.Context, strategy []*SchedulingStrategy) error - // FindCurrentUsingSchedulingStrategiesWithPID finds the current scheduling strategies being used and their associated PIDs - FindCurrentUsingSchedulingStrategiesWithPID(ctx context.Context) ([]*SchedulingStrategy, bool, error) - // GetStrategyCacheStats returns statistics about the strategy cache - GetStrategyCacheStats() map[string]any -} diff --git a/domain/metrics.go b/domain/metrics.go deleted file mode 100644 index 4bb1d55..0000000 --- a/domain/metrics.go +++ /dev/null @@ -1,19 +0,0 @@ -package domain - -import "time" - -// BssData represents the metrics data structure -type BssData struct { - Usersched_last_run_at uint64 `json:"usersched_last_run_at"` // The PID of the userspace scheduler - Nr_queued uint64 `json:"nr_queued"` // Number of tasks queued in the userspace scheduler - Nr_scheduled uint64 `json:"nr_scheduled"` // Number of tasks scheduled by the userspace scheduler - Nr_running uint64 `json:"nr_running"` // Number of tasks currently running in the userspace scheduler - Nr_online_cpus uint64 `json:"nr_online_cpus"` // Number of online CPUs in the system - Nr_user_dispatches uint64 `json:"nr_user_dispatches"` // Number of user-space dispatches - Nr_kernel_dispatches uint64 `json:"nr_kernel_dispatches"` // Number of kernel-space dispatches - Nr_cancel_dispatches uint64 `json:"nr_cancel_dispatches"` // Number of cancelled dispatches - Nr_bounce_dispatches uint64 `json:"nr_bounce_dispatches"` // Number of bounce dispatches - Nr_failed_dispatches uint64 `json:"nr_failed_dispatches"` // Number of failed dispatches - Nr_sched_congested uint64 `json:"nr_sched_congested"` // Number of times the scheduler was congested - UpdatedTime time.Time `json:"-"` // Timestamp of the last update -} diff --git a/domain/pod.go b/domain/pod.go deleted file mode 100644 index b6ce846..0000000 --- a/domain/pod.go +++ /dev/null @@ -1,17 +0,0 @@ -package domain - -// PodProcess represents a process information within a pod -type PodProcess struct { - PID int `json:"pid"` - Command string `json:"command"` - PPID int `json:"ppid,omitempty"` -} - -// PodInfo represents pod information with associated processes -type PodInfo struct { - PodName string `json:"pod_name"` - Namespace string `json:"namespace"` - PodUID string `json:"pod_uid"` - ContainerID string `json:"container_id,omitempty"` - Processes []PodProcess `json:"processes"` -} diff --git a/domain/strategy.go b/domain/strategy.go deleted file mode 100644 index 722ad55..0000000 --- a/domain/strategy.go +++ /dev/null @@ -1,16 +0,0 @@ -package domain - -// LabelSelector represents a key-value pair for pod label selection -type LabelSelector struct { - Key string `json:"key"` // Label key - Value string `json:"value"` // Label value -} - -// SchedulingStrategy represents a strategy for process scheduling -type SchedulingStrategy struct { - Priority bool `json:"priority"` // If true, set vtime to minimum vtime - ExecutionTime uint64 `json:"execution_time"` // Time slice for this process in nanoseconds - PID int `json:"pid,omitempty"` // Process ID to apply this strategy to - Selectors []LabelSelector `json:"selectors,omitempty"` // Label selectors to match pods - CommandRegex string `json:"command_regex,omitempty"` // Regex to match process command -} diff --git a/go.mod b/go.mod index 80636ff..a137420 100644 --- a/go.mod +++ b/go.mod @@ -8,38 +8,28 @@ tool github.com/vektra/mockery/v3 require ( github.com/golang-jwt/jwt/v5 v5.3.0 - github.com/gorilla/mux v1.8.1 + github.com/labstack/echo/v4 v4.13.4 github.com/pkg/errors v0.9.1 - github.com/rs/xid v1.5.0 - github.com/stretchr/testify v1.10.0 + github.com/rs/zerolog v1.33.0 + github.com/spf13/cobra v1.8.1 + github.com/spf13/viper v1.21.0 + github.com/stretchr/testify v1.11.1 go.mongodb.org/mongo-driver/v2 v2.4.0 - k8s.io/api v0.33.2 - k8s.io/apimachinery v0.33.2 - k8s.io/client-go v0.33.2 + go.uber.org/fx v1.24.0 + golang.org/x/crypto v0.45.0 ) require ( github.com/brunoga/deep v1.2.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fatih/structs v1.1.0 // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.23.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/snappy v1.0.0 // indirect - github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jedib0t/go-pretty/v6 v6.6.7 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.16.7 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect github.com/knadh/koanf/parsers/yaml v0.1.0 // indirect @@ -48,23 +38,25 @@ require ( github.com/knadh/koanf/providers/posflag v0.1.0 // indirect github.com/knadh/koanf/providers/structs v0.1.0 // indirect github.com/knadh/koanf/v2 v2.3.0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rs/zerolog v1.33.0 // indirect - github.com/spf13/cobra v1.8.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/objx v0.5.2 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect github.com/vektra/mockery/v3 v3.5.5 // indirect - github.com/x448/float16 v0.8.4 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect @@ -72,26 +64,18 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect - golang.org/x/crypto v0.45.0 // indirect + go.uber.org/dig v1.19.0 // indirect + go.uber.org/goleak v1.3.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.26.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.29.0 // indirect golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect - golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.38.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect - gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect - sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 37a38a1..55e4e9b 100644 --- a/go.sum +++ b/go.sum @@ -2,64 +2,30 @@ github.com/brunoga/deep v1.2.4 h1:Aj9E9oUbE+ccbyh35VC/NHlzzjfIVU69BXu2mt2LmL8= github.com/brunoga/deep v1.2.4/go.mod h1:GDV6dnXqn80ezsLSZ5Wlv1PdKAWAO4L5PnKYtv2dgaI= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -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/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -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/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -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/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= @@ -76,17 +42,17 @@ github.com/knadh/koanf/providers/structs v0.1.0 h1:wJRteCNn1qvLtE5h8KQBvLJovidSd github.com/knadh/koanf/providers/structs v0.1.0/go.mod h1:sw2YZ3txUcqA3Z27gPlmmBzWn1h8Nt9O6EP/91MkcWE= github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM= github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= +github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -97,17 +63,8 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -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= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= -github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= -github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -118,30 +75,39 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -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/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vektra/mockery/v3 v3.5.5 h1:1ExE+yqz3ytvEOe7pUH5VWIwmsYlSq+FjWPVVLdE8O4= github.com/vektra/mockery/v3 v3.5.5/go.mod h1:Oti3Df0WP8wwT31yuVri3QNsDeMUQU5Q4QEg8EabaBw= -github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= -github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -156,52 +122,40 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= -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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver/v2 v2.4.0 h1:Oq6BmUAAFTzMeh6AonuDlgZMuAuEiUxoAD1koK5MuFo= go.mongodb.org/mongo-driver/v2 v2.4.0/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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-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.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -209,68 +163,26 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= 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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= -gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -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.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= -k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= -k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= -k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= -k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= -sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/main.go b/main.go index b66ee90..9160d07 100644 --- a/main.go +++ b/main.go @@ -2,122 +2,65 @@ package main import ( "context" - "errors" - "log/slog" - "net/http" + "log" "os" - "os/signal" - "syscall" - "time" - "github.com/Gthulhu/api/adapter/kubernetes" - "github.com/Gthulhu/api/cache" - "github.com/Gthulhu/api/config" - "github.com/Gthulhu/api/rest" - "github.com/Gthulhu/api/service" - "github.com/Gthulhu/api/util" - "github.com/gorilla/mux" + managerapp "github.com/Gthulhu/api/manager/app" + "github.com/Gthulhu/api/pkg/logger" + "github.com/spf13/cobra" ) -func main() { - // Parse command line options - cmdOptions := ParseCommandLineOptions() - PrintCommandLineOptions(cmdOptions) - - logger := util.GetLogger() +var ( + rootCmd = &cobra.Command{Use: "manager or decisionmaker"} +) - // Load configuration - cfg, err := config.LoadConfig(cmdOptions.ConfigPath) - if err != nil { - logger.Error("Failed to load configuration, exit", util.LogErrAttr(err)) - return - } - jwtRsaKey, err := config.InitJWTRsaKey(cfg.JWT) - if err != nil { - logger.Error("Failed to init jwt rsa key, exit", util.LogErrAttr(err)) - return - } +func init() { + ManagerCmd.Flags().StringP("config-name", "c", "", "Configuration file name without extension") + ManagerCmd.Flags().StringP("config-dir", "d", "", "Configuration file directory path") +} - k8sAdapter, err := kubernetes.NewK8SAdapter(kubernetes.Options{ - KubeConfigPath: cmdOptions.KubeConfigPath, - InCluster: cmdOptions.InCluster, - }) - if err != nil { - logger.Error("Failed to init k8s adapter, exit", util.LogErrAttr(err)) - return +func main() { + rootCmd.AddCommand(ManagerCmd) + if err := rootCmd.Execute(); err != nil { + log.Fatalf("Command execution failed: %v", err) + os.Exit(1) } +} - strategyCache := cache.NewStrategyCache() - stopPodWatcher, err := cache.StartPodWatcher(strategyCache, k8sAdapter.GetClient()) - if err != nil { - logger.Error("Failed to start pod watcher, exit", util.LogErrAttr(err)) - return - } +// GrpcServerCmd is the command to start the gRPC server +var ManagerCmd = &cobra.Command{ + Run: RunManagerApp, + Use: "manager", +} - svc, err := service.NewService(context.Background(), service.Params{ - K8sAdapter: k8sAdapter, - JWTPrivateKey: jwtRsaKey, - Config: cfg, - StrategyCache: strategyCache, - }) +func RunManagerApp(cmd *cobra.Command, args []string) { + configName, configDirPath := getConfigInfo(cmd) + logger.InitLogger() + app, err := managerapp.NewRestApp(configName, configDirPath) if err != nil { - logger.Error("Failed to create service, exit", util.LogErrAttr(err)) - return + logger.Logger(context.Background()).Fatal().Err(err).Msg("failed to create rest app") } + app.Run() +} - hdl := rest.NewHandler(rest.Params{ - Service: svc, - JWTPrivateKey: jwtRsaKey, - Config: cfg, - }) - - // If port is specified in command line, override the port in config file - port := cfg.Server.Port - if cmdOptions.Port != "" { - port = cmdOptions.Port - logger.Info("Overriding server port from command line", slog.String("port", port)) +func getConfigInfo(cmd *cobra.Command) (string, string) { + configName := "manager_config" + configDirPath := "" + if cmd != nil { + configNameFlag, err := cmd.Flags().GetString("config-name") + if err == nil && configNameFlag != "" { + configName = configNameFlag + } + configPathFlag, err := cmd.Flags().GetString("config-dir") + if err == nil && configPathFlag != "" { + configDirPath = configPathFlag + } } - - // Create router - r := mux.NewRouter() - - // Server configuration - serverPort := port - logger.Info("Starting BSS Metrics API Server", slog.String("port", serverPort)) - logger.Info("JWT Authentication: Enabled", slog.Int("token_duration_hours", cfg.JWT.TokenDuration)) - // Setup routes - rest.SetupRoutes(r, hdl) - - // Start server - srv := &http.Server{ - Handler: r, - Addr: serverPort, - WriteTimeout: time.Duration(cfg.Server.WriteTimeout) * time.Second, - ReadTimeout: time.Duration(cfg.Server.ReadTimeout) * time.Second, - IdleTimeout: time.Duration(cfg.Server.IdleTimeout) * time.Second, + if envConfigName := os.Getenv("MANAGER_CONFIG_NAME"); envConfigName != "" { + configName = envConfigName } - - go func() { - if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { - logger.Error("Failed to start server, exit", util.LogErrAttr(err)) - os.Exit(1) - } - }() - - // Wait for interrupt signal to gracefully shutdown the server - ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) - defer stop() - - <-ctx.Done() - logger.Info("Shutting down server...") - close(stopPodWatcher) - - shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - if err := srv.Shutdown(shutdownCtx); err != nil { - logger.Error("Server forced to shutdown", util.LogErrAttr(err)) - } else { - logger.Info("Server exited gracefully") + if envConfigPath := os.Getenv("MANAGER_CONFIG_DIR_PATH"); envConfigPath != "" { + configDirPath = envConfigPath } + return configName, configDirPath } diff --git a/manager/app/module.go b/manager/app/module.go new file mode 100644 index 0000000..409189c --- /dev/null +++ b/manager/app/module.go @@ -0,0 +1,73 @@ +package app + +import ( + "github.com/Gthulhu/api/config" + "github.com/Gthulhu/api/manager/repository" + "github.com/Gthulhu/api/manager/rest" + "github.com/Gthulhu/api/manager/service" + "go.uber.org/fx" +) + +func ConfigModule(configName string, configPath string) (fx.Option, error) { + cfg, err := config.InitManagerConfig(configName, configPath) + if err != nil { + return nil, err + } + + return fx.Options( + fx.Provide(func() config.ManageConfig { + return cfg + }), + fx.Provide(func(managerCfg config.ManageConfig) config.MongoDBConfig { + return managerCfg.MongoDB + }), + fx.Provide(func(managerCfg config.ManageConfig) config.ServerConfig { + return managerCfg.Server + }), + fx.Provide(func(managerCfg config.ManageConfig) config.KeyConfig { + return managerCfg.Key + }), + fx.Provide(func(managerCfg config.ManageConfig) config.AccountConfig { + return managerCfg.Account + }), + ), nil +} + +// RepoModule creates an Fx module that provides the repository layer, return repository.Repository +func RepoModule(configName string, configPath string) (fx.Option, error) { + configModule, err := ConfigModule(configName, configPath) + if err != nil { + return nil, err + } + + return fx.Options( + configModule, + fx.Provide(repository.NewRepository), + ), nil +} + +// ServiceModule creates an Fx module that provides the service layer, return domain.Service +func ServiceModule(configName string, configPath string) (fx.Option, error) { + repoModule, err := RepoModule(configName, configPath) + if err != nil { + return nil, err + } + + return fx.Options( + repoModule, + fx.Provide(service.NewService), + ), nil +} + +// HandlerModule creates an Fx module that provides the REST handler, return *rest.Handler +func HandlerModule(configName string, configPath string) (fx.Option, error) { + serviceModule, err := ServiceModule(configName, configPath) + if err != nil { + return nil, err + } + + return fx.Options( + serviceModule, + fx.Provide(rest.NewHandler), + ), nil +} diff --git a/manager/app/rest_app.go b/manager/app/rest_app.go new file mode 100644 index 0000000..3c489bb --- /dev/null +++ b/manager/app/rest_app.go @@ -0,0 +1,77 @@ +package app + +import ( + "context" + "os" + + "github.com/Gthulhu/api/config" + "github.com/Gthulhu/api/manager/rest" + "github.com/Gthulhu/api/pkg/logger" + "github.com/labstack/echo/v4" + "github.com/spf13/cobra" + "go.uber.org/fx" +) + +func NewRestApp(configName string, configDirPath string) (*fx.App, error) { + handlerModule, err := HandlerModule(configName, configDirPath) + if err != nil { + return nil, err + } + + app := fx.New( + handlerModule, + fx.Invoke(StartRestApp), + ) + return app, nil +} + +func StartRestApp(lc fx.Lifecycle, cfg config.ServerConfig, handler *rest.Handler) error { + engine := echo.New() + handler.SetupRoutes(engine) + + // TODO: setup middleware, logging, etc. + + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + serverHost := cfg.Host + if serverHost == "" { + serverHost = ":8080" + } + go func() { + logger.Logger(ctx).Info().Msgf("starting rest server on port %s", serverHost) + if err := engine.Start(serverHost); err != nil { + logger.Logger(ctx).Fatal().Msgf("start rest server fail on port %s", serverHost) + } + }() + return nil + }, + OnStop: func(ctx context.Context) error { + logger.Logger(ctx).Info().Msg("shutting down rest server") + return engine.Shutdown(ctx) + }, + }) + + return nil +} + +func getConfigInfo(cmd *cobra.Command) (string, string) { + configName := "manager_config" + configDirPath := "" + if cmd != nil { + configNameFlag, err := cmd.Flags().GetString("config-name") + if err == nil && configNameFlag != "" { + configName = configNameFlag + } + configPathFlag, err := cmd.Flags().GetString("config-dir") + if err == nil && configPathFlag != "" { + configDirPath = configPathFlag + } + } + if envConfigName := os.Getenv("MANAGER_CONFIG_NAME"); envConfigName != "" { + configName = envConfigName + } + if envConfigPath := os.Getenv("MANAGER_CONFIG_DIR_PATH"); envConfigPath != "" { + configDirPath = envConfigPath + } + return configName, configDirPath +} diff --git a/manager/cmd/.keep b/manager/cmd/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/manager/domain/.keep b/manager/domain/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/manager/domain/interface.go b/manager/domain/interface.go index 8b412a9..98120c2 100644 --- a/manager/domain/interface.go +++ b/manager/domain/interface.go @@ -50,7 +50,7 @@ type Service interface { SignUp(ctx context.Context, email, password string) error Login(ctx context.Context, email, password string) (string, error) Logout(ctx context.Context, token string) error - ChangePassword(ctx context.Context, email string, oldPassword, newPassword EncryptedPassword) error + ChangePassword(ctx context.Context, email string, oldPassword, newPassword string) error CreateUser(ctx context.Context, operator Claims, user *User) error DeleteUser(ctx context.Context, operator Claims, userID bson.ObjectID) error UpdateUser(ctx context.Context, operator Claims, user *User) error diff --git a/manager/repository/.keep b/manager/repository/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/manager/repository/repo.go b/manager/repository/repo.go index 47b9900..9216ec9 100644 --- a/manager/repository/repo.go +++ b/manager/repository/repo.go @@ -2,33 +2,59 @@ package repository import ( "context" + "crypto/tls" + "crypto/x509" "errors" "fmt" "time" + "github.com/Gthulhu/api/config" "github.com/Gthulhu/api/manager/domain" + "go.uber.org/fx" + "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" ) type Params struct { - Client *mongo.Client - Database string + fx.In + MongoConfig config.MongoDBConfig } func NewRepository(params Params) (domain.Repository, error) { - if params.Client == nil { - return nil, errors.New("mongo client is required") + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + mongoOpts := options.Client().ApplyURI(params.MongoConfig.URI) + if params.MongoConfig.CAPem != "" { + caPool := x509.NewCertPool() + caPool.AppendCertsFromPEM([]byte(params.MongoConfig.CAPem)) + tlsConfig := &tls.Config{ + RootCAs: caPool, + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: false, + } + mongoOpts.SetTLSConfig(tlsConfig) + } + + client, err := mongo.Connect() + if err != nil { + return nil, fmt.Errorf("connect to mongodb: %w", err) + } + + if err := client.Ping(ctx, nil); err != nil { + return nil, fmt.Errorf("ping mongodb: %w", err) } - dbName := params.Database + dbName := params.MongoConfig.Database if dbName == "" { dbName = "manager" } return &repo{ - client: params.Client, - db: params.Client.Database(dbName), + client: client, + db: client.Database(dbName), }, nil } diff --git a/manager/rest/.keep b/manager/rest/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/manager/rest/auth_hdl.go b/manager/rest/auth_hdl.go new file mode 100644 index 0000000..3faff46 --- /dev/null +++ b/manager/rest/auth_hdl.go @@ -0,0 +1,23 @@ +package rest + +import "net/http" + +func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) { + +} + +func (h *Handler) DeleteUser(w http.ResponseWriter, r *http.Request) { + +} + +func (h *Handler) UpdateUser(w http.ResponseWriter, r *http.Request) { + +} + +func (h *Handler) ChangePassword(w http.ResponseWriter, r *http.Request) { + +} + +func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { + +} diff --git a/rest/handler.go b/manager/rest/hander.go similarity index 63% rename from rest/handler.go rename to manager/rest/hander.go index 0a96801..aeb51c3 100644 --- a/rest/handler.go +++ b/manager/rest/hander.go @@ -1,14 +1,14 @@ package rest import ( - "crypto/rsa" + "context" "encoding/json" "net/http" "time" - "github.com/Gthulhu/api/config" - "github.com/Gthulhu/api/domain" - "github.com/Gthulhu/api/util" + "github.com/Gthulhu/api/manager/domain" + "github.com/Gthulhu/api/pkg/logger" + "go.uber.org/fx" ) // ErrorResponse represents error response structure @@ -25,31 +25,26 @@ type SuccessResponse struct { } type Params struct { - Service domain.Service - JWTPrivateKey *rsa.PrivateKey - Config *config.Config + fx.In + Svc domain.Service } -func NewHandler(params Params) *Handler { +func NewHandler(params Params) (*Handler, error) { return &Handler{ - Service: params.Service, - jwtPrivateKey: params.JWTPrivateKey, - Config: params.Config, - } + Svc: params.Svc, + }, nil } type Handler struct { - domain.Service - Config *config.Config - jwtPrivateKey *rsa.PrivateKey + Svc domain.Service } -func (h *Handler) JSONResponse(w http.ResponseWriter, status int, data any) { +func (h *Handler) JSONResponse(ctx context.Context, w http.ResponseWriter, status int, data any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) err := json.NewEncoder(w).Encode(data) if err != nil { - util.GetLogger().Error("Failed to encode JSON response", util.LogErrAttr(err)) + logger.Logger(ctx).Error().Err(err).Msg("Failed to encode JSON response") http.Error(w, "Failed to encode JSON response", http.StatusInternalServerError) } } @@ -63,21 +58,21 @@ func (h *Handler) JSONBind(r *http.Request, dst any) error { return nil } -func (h *Handler) ErrorResponse(w http.ResponseWriter, status int, errMsg string) { +func (h *Handler) ErrorResponse(ctx context.Context, w http.ResponseWriter, status int, errMsg string) { resp := ErrorResponse{ Success: false, Error: errMsg, } - h.JSONResponse(w, status, resp) + h.JSONResponse(ctx, w, status, resp) } -func (h *Handler) SuccessResponse(w http.ResponseWriter, message string) { +func (h *Handler) SuccessResponse(ctx context.Context, w http.ResponseWriter, message string) { resp := SuccessResponse{ Success: true, Message: message, Timestamp: time.Now().UTC().Format(time.RFC3339), } - h.JSONResponse(w, http.StatusOK, resp) + h.JSONResponse(ctx, w, http.StatusOK, resp) } func (h *Handler) Version(w http.ResponseWriter, r *http.Request) { @@ -86,14 +81,14 @@ func (h *Handler) Version(w http.ResponseWriter, r *http.Request) { "version": "1.0.0", "endpoints": "/api/v1/auth/token (POST), /api/v1/metrics (POST), /api/v1/pods/pids (GET), /api/v1/scheduling/strategies (GET, POST), /health (GET), /static/ (Frontend)", } - h.JSONResponse(w, http.StatusOK, response) + h.JSONResponse(r.Context(), w, http.StatusOK, response) } func (h *Handler) HealthCheck(w http.ResponseWriter, r *http.Request) { - response := map[string]interface{}{ + response := map[string]any{ "status": "healthy", "timestamp": time.Now().UTC().Format(time.RFC3339), "service": "BSS Metrics API Server", } - h.JSONResponse(w, http.StatusOK, response) + h.JSONResponse(r.Context(), w, http.StatusOK, response) } diff --git a/manager/rest/handler_test.go b/manager/rest/handler_test.go new file mode 100644 index 0000000..669a7a6 --- /dev/null +++ b/manager/rest/handler_test.go @@ -0,0 +1,63 @@ +package rest_test + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/Gthulhu/api/config" + "github.com/Gthulhu/api/manager/app" + "github.com/Gthulhu/api/manager/rest" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/suite" + "go.uber.org/fx" +) + +func TestHandlerTestSuite(t *testing.T) { + suite.Run(t, new(HandlerTestSuite)) +} + +type HandlerTestSuite struct { + suite.Suite + Handler *rest.Handler + Ctx context.Context + Engine *echo.Echo +} + +func (suite *HandlerTestSuite) SetupSuite() { + suite.Ctx = context.Background() + handlerModule, err := app.HandlerModule("manager_config.test.toml", config.GetAbsPath("config")) + suite.Require().NoError(err, "Failed to create handler module") + opt := fx.Options( + handlerModule, + fx.Populate(&suite.Handler), + ) + + err = fx.New(opt).Start(suite.Ctx) + suite.Require().NoError(err, "Failed to start Fx app") + suite.Require().NotNil(suite.Handler, "Handler should not be nil") + e := echo.New() + e.HideBanner = true + e.HidePort = true + suite.Engine = e + suite.Handler.SetupRoutes(e) +} + +func (suite *HandlerTestSuite) JSONDecode(r *httptest.ResponseRecorder, dst any) { + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(dst) + suite.Require().NoError(err, "Failed to decode JSON response") +} + +func (suite *HandlerTestSuite) TestHealthCheck() { + req := httptest.NewRequest(http.MethodGet, "/health", nil) + rec := httptest.NewRecorder() + suite.Engine.ServeHTTP(rec, req) + + suite.Equal(http.StatusOK, rec.Code, "Expected status OK") + var resp map[string]any + suite.JSONDecode(rec, &resp) + suite.Equal("healthy", resp["status"].(string), "Expected status to be healthy") +} diff --git a/manager/rest/routes.go b/manager/rest/routes.go new file mode 100644 index 0000000..ac7aa5f --- /dev/null +++ b/manager/rest/routes.go @@ -0,0 +1,36 @@ +package rest + +import ( + "net/http" + + "github.com/labstack/echo/v4" +) + +func (h *Handler) SetupRoutes(engine *echo.Echo) { + engine.GET("/health", h.echoHandler(h.HealthCheck)) + engine.GET("/version", h.echoHandler(h.Version)) + + // v1 routes + { + // users & auth routes + engine.POST("/api/v1/users", h.echoHandler(h.CreateUser), echo.WrapMiddleware(h.AuthMiddleware)) + engine.DELETE("/api/v1/users", h.echoHandler(h.DeleteUser), echo.WrapMiddleware(h.AuthMiddleware)) + engine.PUT("/api/v1/users", h.echoHandler(h.UpdateUser), echo.WrapMiddleware(h.AuthMiddleware)) + engine.PUT("/api/v1/users/password", h.echoHandler(h.ChangePassword), echo.WrapMiddleware(h.AuthMiddleware)) + engine.POST("/api/v1/auth/login", h.echoHandler(h.Login), echo.WrapMiddleware(h.AuthMiddleware)) + } + +} + +func (h *Handler) echoHandler(handlerFunc func(w http.ResponseWriter, r *http.Request)) echo.HandlerFunc { + return echo.WrapHandler(http.HandlerFunc(handlerFunc)) +} + +func (h *Handler) AuthMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Authentication logic here (e.g., check JWT token) + + // If authenticated, proceed to the next handler + next.ServeHTTP(w, r) + }) +} diff --git a/manager/service/.keep b/manager/service/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/manager/service/svc.go b/manager/service/svc.go index 2844767..2f1e74d 100644 --- a/manager/service/svc.go +++ b/manager/service/svc.go @@ -1,12 +1,84 @@ package service import ( + "context" "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "github.com/Gthulhu/api/config" "github.com/Gthulhu/api/manager/domain" + "go.uber.org/fx" ) +type Params struct { + fx.In + Repo domain.Repository + KeyConfig config.KeyConfig + AccountConfig config.AccountConfig +} + +func NewService(params Params) (domain.Service, error) { + jwtPrivateKey, err := initRSAPrivateKey(params.KeyConfig.RsaPrivateKeyPem) + if err != nil { + return nil, fmt.Errorf("initialize RSA private key: %w", err) + } + + svc := &Service{ + Repo: params.Repo, + jwtPrivateKey: jwtPrivateKey, + } + + return svc, nil +} + type Service struct { Repo domain.Repository jwtPrivateKey *rsa.PrivateKey } + +func initRSAPrivateKey(pemStr string) (*rsa.PrivateKey, error) { + block, _ := pem.Decode([]byte(pemStr)) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM block containing private key") + } + + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + // Try PKCS8 format + keyInterface, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse private key: %v", err) + } + var ok bool + key, ok = keyInterface.(*rsa.PrivateKey) + if !ok { + return nil, fmt.Errorf("private key is not RSA") + } + } + return key, nil +} + +func (svc Service) ListAuditLogs(ctx context.Context, opt *domain.QueryAuditLogOptions) error { + return errors.New("not implemented") +} +func (svc Service) CreateRole(ctx context.Context, role *domain.Role) error { + return errors.New("not implemented") +} +func (svc Service) UpdateRole(ctx context.Context, role *domain.Role) error { + return errors.New("not implemented") +} +func (svc Service) QueryRoles(ctx context.Context, opt *domain.QueryRoleOptions) error { + return errors.New("not implemented") +} +func (svc Service) CreatePermission(ctx context.Context, permission *domain.Permission) error { + return errors.New("not implemented") +} +func (svc Service) UpdatePermission(ctx context.Context, permission *domain.Permission) error { + return errors.New("not implemented") +} +func (svc Service) QueryPermissions(ctx context.Context, opt *domain.QueryPermissionOptions) error { + return errors.New("not implemented") +} diff --git a/options.go b/options.go deleted file mode 100644 index 47af094..0000000 --- a/options.go +++ /dev/null @@ -1,68 +0,0 @@ -package main - -import ( - "flag" - "log" - "log/slog" - "os" - "path/filepath" - - "github.com/Gthulhu/api/util" -) - -// CommandLineOptions contains all command line options -type CommandLineOptions struct { - ConfigPath string - Port string - KubeConfigPath string - InCluster bool -} - -// ParseCommandLineOptions parses command line arguments -func ParseCommandLineOptions() CommandLineOptions { - options := CommandLineOptions{} - - // Define command line flags - flag.StringVar(&options.ConfigPath, "config", "config.json", "Path to configuration file") - flag.StringVar(&options.Port, "port", "", "Server port (overrides config file)") - flag.StringVar(&options.KubeConfigPath, "kubeconfig", "", "Path to Kubernetes config file (defaults to $HOME/.kube/config)") - flag.BoolVar(&options.InCluster, "in-cluster", false, "Run in Kubernetes in-cluster mode") - - // Parse flags - flag.Parse() - - // If kubeconfig is not specified, check environment variable - if options.KubeConfigPath == "" { - options.KubeConfigPath = os.Getenv("KUBECONFIG") - } - - // If still empty, use default path - if options.KubeConfigPath == "" && !options.InCluster { - homeDir, err := os.UserHomeDir() - if err == nil { - options.KubeConfigPath = filepath.Join(homeDir, ".kube", "config") - } - } - - return options -} - -// PrintCommandLineOptions prints the current command line options -func PrintCommandLineOptions(options CommandLineOptions) { - logger := util.GetLogger() - logger = logger.With(slog.String("config_path", options.ConfigPath)) - if options.Port != "" { - logger = logger.With(slog.String("port_override", options.Port)) - } - - if options.InCluster { - logger = logger.With(slog.Bool("k8s_in_cluster_mode", options.InCluster)) - log.Printf(" Kubernetes: In-cluster mode") - } else if options.KubeConfigPath != "" { - logger = logger.With(slog.Bool("k8s_in_cluster_mode", false)) - logger = logger.With(slog.String("k8s_config_path", options.KubeConfigPath)) - } else { - logger.Info("Kubernetes: No config specified") - } - logger.Info("parsed command line options") -} diff --git a/rest/metrics_hdl.go b/rest/metrics_hdl.go deleted file mode 100644 index 15aea1b..0000000 --- a/rest/metrics_hdl.go +++ /dev/null @@ -1,116 +0,0 @@ -package rest - -import ( - "errors" - "log/slog" - "net/http" - "time" - - "github.com/Gthulhu/api/domain" - "github.com/Gthulhu/api/service" - "github.com/Gthulhu/api/util" -) - -// SaveMetricsRequest represents the request structure for saving BSS metrics -type SaveMetricsRequest struct { - Usersched_last_run_at uint64 `json:"usersched_last_run_at"` // The PID of the userspace scheduler - Nr_queued uint64 `json:"nr_queued"` // Number of tasks queued in the userspace scheduler - Nr_scheduled uint64 `json:"nr_scheduled"` // Number of tasks scheduled by the userspace scheduler - Nr_running uint64 `json:"nr_running"` // Number of tasks currently running in the userspace scheduler - Nr_online_cpus uint64 `json:"nr_online_cpus"` // Number of online CPUs in the system - Nr_user_dispatches uint64 `json:"nr_user_dispatches"` // Number of user-space dispatches - Nr_kernel_dispatches uint64 `json:"nr_kernel_dispatches"` // Number of kernel-space dispatches - Nr_cancel_dispatches uint64 `json:"nr_cancel_dispatches"` // Number of cancelled dispatches - Nr_bounce_dispatches uint64 `json:"nr_bounce_dispatches"` // Number of bounce dispatches - Nr_failed_dispatches uint64 `json:"nr_failed_dispatches"` // Number of failed dispatches - Nr_sched_congested uint64 `json:"nr_sched_congested"` // Number of times the scheduler was congested -} - -func (req *SaveMetricsRequest) LogValue() slog.Value { - return slog.GroupValue( - slog.Uint64("usersched_last_run_at", req.Usersched_last_run_at), - slog.Uint64("nr_queued", req.Nr_queued), - slog.Uint64("nr_scheduled", req.Nr_scheduled), - slog.Uint64("nr_running", req.Nr_running), - slog.Uint64("nr_online_cpus", req.Nr_online_cpus), - slog.Uint64("nr_user_dispatches", req.Nr_user_dispatches), - slog.Uint64("nr_kernel_dispatches", req.Nr_kernel_dispatches), - slog.Uint64("nr_cancel_dispatches", req.Nr_cancel_dispatches), - slog.Uint64("nr_bounce_dispatches", req.Nr_bounce_dispatches), - slog.Uint64("nr_failed_dispatches", req.Nr_failed_dispatches), - slog.Uint64("nr_sched_congested", req.Nr_sched_congested), - ) -} - -// SaveMetricsHandler handles saving BSS metrics data -func (h *Handler) SaveMetricsHandler(w http.ResponseWriter, r *http.Request) { - var req SaveMetricsRequest - err := h.JSONBind(r, &req) - if err != nil { - h.ErrorResponse(w, http.StatusBadRequest, "Invalid JSON format: "+err.Error()) - return - } - - bssData := domain.BssData{ - Usersched_last_run_at: req.Usersched_last_run_at, - Nr_queued: req.Nr_queued, - Nr_scheduled: req.Nr_scheduled, - Nr_running: req.Nr_running, - Nr_online_cpus: req.Nr_online_cpus, - Nr_user_dispatches: req.Nr_user_dispatches, - Nr_kernel_dispatches: req.Nr_kernel_dispatches, - Nr_cancel_dispatches: req.Nr_cancel_dispatches, - Nr_bounce_dispatches: req.Nr_bounce_dispatches, - Nr_failed_dispatches: req.Nr_failed_dispatches, - Nr_sched_congested: req.Nr_sched_congested, - UpdatedTime: time.Now(), - } - - err = h.Service.SaveBSSMetrics(r.Context(), &bssData) - if err != nil { - h.ErrorResponse(w, http.StatusInternalServerError, "Failed to save metrics: "+err.Error()) - return - } - - util.GetLogger().Info("Saved BSS metrics", slog.Any("metrics", req)) - h.SuccessResponse(w, "Metrics saved successfully") -} - -// GetMetricsResponse represents the response structure for getting current metrics -type GetMetricsResponse struct { - Success bool `json:"success"` - Message string `json:"message"` - Timestamp string `json:"timestamp"` - Data *domain.BssData `json:"data,omitempty"` - MetricsTimestamp string `json:"metrics_timestamp,omitempty"` -} - -// GetMetricsHandler handles retrieving the latest BSS metrics data -func (h *Handler) GetMetricsHandler(w http.ResponseWriter, r *http.Request) { - bssData, err := h.Service.GetBSSMetrics(r.Context()) - if err != nil { - if errors.Is(err, service.ErrNoBssData) { - response := GetMetricsResponse{ - Success: false, - Message: "No metrics data available yet", - Timestamp: time.Now().UTC().Format(time.RFC3339), - } - h.JSONResponse(w, http.StatusOK, response) - return - } - - h.ErrorResponse(w, http.StatusInternalServerError, "Failed to get metrics: "+err.Error()) - return - } - - util.GetLogger().Info("Retrieved BSS metrics") - - resp := GetMetricsResponse{ - Success: true, - Message: "Metrics retrieved successfully", - Timestamp: time.Now().UTC().Format(time.RFC3339), - Data: bssData, - MetricsTimestamp: bssData.UpdatedTime.UTC().Format(time.RFC3339), - } - h.JSONResponse(w, http.StatusOK, resp) -} diff --git a/rest/middleware.go b/rest/middleware.go deleted file mode 100644 index bee34ce..0000000 --- a/rest/middleware.go +++ /dev/null @@ -1,188 +0,0 @@ -package rest - -import ( - "crypto/rsa" - "encoding/json" - "fmt" - "log" - "log/slog" - "net/http" - "strings" - "time" - - "github.com/Gthulhu/api/util" - "github.com/golang-jwt/jwt/v5" - "github.com/rs/xid" -) - -// getJwtAuthMiddleware returns a middleware that validates JWT tokens -func getJwtAuthMiddleware(rasKey *rsa.PrivateKey) func(next http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Skip auth for OPTIONS requests, health check, root endpoint, token endpoint, and static files - if r.Method == "OPTIONS" || - r.URL.Path == "/health" || - r.URL.Path == "/" || - r.URL.Path == "/api/v1/auth/token" || - strings.HasPrefix(r.URL.Path, "/static/") { - next.ServeHTTP(w, r) - return - } - - // Extract token from Authorization header - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusUnauthorized) - if err := json.NewEncoder(w).Encode(ErrorResponse{ - Success: false, - Error: "Authorization header is required", - }); err != nil { - log.Printf("Error encoding response: %v", err) - } - return - } - - // Check Bearer token format - const bearerSchema = "Bearer " - if !strings.HasPrefix(authHeader, bearerSchema) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusUnauthorized) - if err := json.NewEncoder(w).Encode(ErrorResponse{ - Success: false, - Error: "Authorization header must start with 'Bearer '", - }); err != nil { - log.Printf("Error encoding response: %v", err) - } - return - } - - tokenString := authHeader[len(bearerSchema):] - - // Validate JWT token - claims, err := validateJWT(rasKey, tokenString) - if err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusUnauthorized) - if err := json.NewEncoder(w).Encode(ErrorResponse{ - Success: false, - Error: "Invalid or expired token: " + err.Error(), - }); err != nil { - log.Printf("Error encoding response: %v", err) - } - return - } - - log.Printf("Authenticated request from client: %s", claims.ClientID) - next.ServeHTTP(w, r) - }) - } -} - -// Claims represents JWT token claims -type Claims struct { - ClientID string `json:"client_id"` - jwt.RegisteredClaims -} - -// validateJWT validates a JWT token and returns the claims -func validateJWT(rasKey *rsa.PrivateKey, tokenString string) (*Claims, error) { - token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return &rasKey.PublicKey, nil - }) - - if err != nil { - return nil, err - } - - if claims, ok := token.Claims.(*Claims); ok && token.Valid { - return claims, nil - } - - return nil, fmt.Errorf("invalid token") -} - -// CORS middleware -func enableCORS(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS") - // Allow Authorization for JWT, and X-Request-ID for tracing - w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Request-ID") - - if r.Method == "OPTIONS" { - w.WriteHeader(http.StatusOK) - return - } - - next.ServeHTTP(w, r) - }) -} - -// Logging middleware -func loggingMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - start := time.Now() - logger := util.GetLogger() - - ctx := r.Context() - logger = logger.With(slog.String("method", r.Method), slog.String("request_uri", r.RequestURI), slog.String("remote_addr", r.RemoteAddr)) - - reqID := r.Header.Get("X-Request-ID") - if reqID == "" { - reqID = xid.New().String() - } - logger = logger.With(slog.String("request_id", reqID)) - r = r.WithContext(util.AddLoggerToCtx(ctx, logger)) - logger.Debug("access log") - log.Printf("%s %s %s", r.Method, r.RequestURI, r.RemoteAddr) - - rec := &statusRecorder{ResponseWriter: w} - - next.ServeHTTP(rec, r) - - if rec.Status >= 500 { - logger.Warn("response log", - slog.Int("status", rec.Status), - slog.Int("response_bytes", rec.Bytes), - slog.Duration("duration", time.Since(start)), - ) - } else if rec.Status >= 400 { - logger.Error("response log", - slog.Int("status", rec.Status), - slog.Int("response_bytes", rec.Bytes), - slog.Duration("duration", time.Since(start)), - ) - } else { - logger.Info("response log", - slog.Int("status", rec.Status), - slog.Int("response_bytes", rec.Bytes), - slog.Duration("duration", time.Since(start)), - ) - } - }) -} - -type statusRecorder struct { - http.ResponseWriter - Status int - Bytes int -} - -func (rec *statusRecorder) WriteHeader(code int) { - rec.Status = code - rec.ResponseWriter.WriteHeader(code) -} - -func (rec *statusRecorder) Write(b []byte) (int, error) { - // 如果沒有明確呼叫 WriteHeader 就是 200 - if rec.Status == 0 { - rec.Status = http.StatusOK - } - n, err := rec.ResponseWriter.Write(b) - rec.Bytes += n - return n, err -} diff --git a/rest/pod_hdl.go b/rest/pod_hdl.go deleted file mode 100644 index 7f26cf8..0000000 --- a/rest/pod_hdl.go +++ /dev/null @@ -1,36 +0,0 @@ -package rest - -import ( - "log/slog" - "net/http" - "time" - - "github.com/Gthulhu/api/domain" - "github.com/Gthulhu/api/util" -) - -// PodPidResponse represents the response structure for pod-pid mapping -type GetPodPidResponse struct { - Success bool `json:"success"` - Message string `json:"message"` - Timestamp string `json:"timestamp"` - Pods []*domain.PodInfo `json:"pods"` -} - -func (h *Handler) GetPodPidHandler(w http.ResponseWriter, r *http.Request) { - podInfos, err := h.Service.GetAllPodInfos(r.Context()) - if err != nil { - h.ErrorResponse(w, http.StatusInternalServerError, "Failed to get pod infos: "+err.Error()) - return - } - - util.GetLogger().Debug("Retrieved pod-pid mappings", slog.Int("pod_count", len(podInfos))) - - resp := GetPodPidResponse{ - Success: true, - Message: "Pod-PID mappings retrieved successfully", - Timestamp: time.Now().UTC().Format(time.RFC3339), - Pods: podInfos, - } - h.JSONResponse(w, http.StatusOK, resp) -} diff --git a/rest/routes.go b/rest/routes.go deleted file mode 100644 index cbc4800..0000000 --- a/rest/routes.go +++ /dev/null @@ -1,52 +0,0 @@ -package rest - -import ( - "net/http" - "path/filepath" - "runtime" - - "github.com/Gthulhu/api/util" - "github.com/gorilla/mux" -) - -func SetupRoutes(route *mux.Router, h *Handler) { - - route.Use(loggingMiddleware) - route.Use(enableCORS) - route.Use(getJwtAuthMiddleware(h.jwtPrivateKey)) // Add JWT authentication middleware - - route.HandleFunc("/api/v1/auth/token", h.GenTokenHandler).Methods("POST", "OPTIONS") - - route.HandleFunc("/api/v1/metrics", h.SaveMetricsHandler).Methods("POST", "OPTIONS") - route.HandleFunc("/api/v1/metrics", h.GetMetricsHandler).Methods("GET", "OPTIONS") - - route.HandleFunc("/api/v1/pods/pids", h.GetPodPidHandler).Methods("GET", "OPTIONS") - - route.HandleFunc("/api/v1/scheduling/strategies", h.GetSchedulingStrategiesHandler).Methods("GET", "OPTIONS") - route.HandleFunc("/api/v1/scheduling/strategies", h.SaveSchedulingStrategiesHandler).Methods("POST", "OPTIONS") - - route.HandleFunc("/health", h.HealthCheck).Methods("GET") - route.HandleFunc("/", h.Version).Methods("GET") - - setupStaticRoutes(route) - - logger := util.GetLogger() - logger.Info("Endpoints:") - logger.Info(" POST /api/v1/auth/token - Generate JWT token") - logger.Info(" POST /api/v1/metrics - Submit metrics data") - logger.Info(" GET /api/v1/metrics - Get current metrics") - logger.Info(" GET /api/v1/pods/pids - Get pod-PID mappings") - logger.Info(" GET /api/v1/scheduling/strategies - Get scheduling strategies") - logger.Info(" POST /api/v1/scheduling/strategies - Save scheduling strategies") - logger.Info(" GET /health - Health check") - logger.Info(" GET /static/ - Frontend web interface") - logger.Info(" GET / - Redirect to frontend") -} - -func setupStaticRoutes(route *mux.Router) { - _, f, _, _ := runtime.Caller(0) - staticFolder := filepath.Join(filepath.Dir(f), "../", "static") - - staticFS := http.FileServer(http.Dir(staticFolder)) - route.PathPrefix("/static/").Handler(http.StripPrefix("/static/", staticFS)).Methods("GET") -} diff --git a/rest/strategies_hdl.go b/rest/strategies_hdl.go deleted file mode 100644 index e64cb60..0000000 --- a/rest/strategies_hdl.go +++ /dev/null @@ -1,72 +0,0 @@ -package rest - -import ( - "fmt" - "net/http" - "time" - - "github.com/Gthulhu/api/domain" - "github.com/Gthulhu/api/util" -) - -// GetSchedulingStrategiesResponse represents the response structure for scheduling strategies -type GetSchedulingStrategiesResponse struct { - Success bool `json:"success"` - Message string `json:"message"` - Timestamp string `json:"timestamp"` - Scheduling []*domain.SchedulingStrategy `json:"scheduling"` -} - -// GetSchedulingStrategiesHandler handles the retrieval of current scheduling strategies -func (h *Handler) GetSchedulingStrategiesHandler(w http.ResponseWriter, r *http.Request) { - finalStrategies, fromCache, err := h.Service.FindCurrentUsingSchedulingStrategiesWithPID(r.Context()) - if err != nil { - util.GetLogger().Error("Failed to get scheduling strategies", util.LogErrAttr(err)) - h.ErrorResponse(w, http.StatusInternalServerError, "Failed to get scheduling strategies: "+err.Error()) - return - } - - // If not from cache, strategies were recalculated in GetCachedStrategies - var message string - if fromCache { - message = "Scheduling strategies retrieved from cache" - } else { - message = "Scheduling strategies recalculated due to pod changes" - } - - cacheStats := h.Service.GetStrategyCacheStats() - // Add cache stats as header for debugging - w.Header().Set("X-Cache-Hit", fmt.Sprintf("%v", fromCache)) - w.Header().Set("X-Cache-Stats", fmt.Sprintf("hits=%d,misses=%d,hit_rate=%v", - cacheStats["hits"], cacheStats["misses"], cacheStats["hit_rate"])) - - // Send success response with cache info - response := GetSchedulingStrategiesResponse{ - Success: true, - Message: message, - Timestamp: time.Now().UTC().Format(time.RFC3339), - Scheduling: finalStrategies, - } - h.JSONResponse(w, http.StatusOK, response) -} - -// StrategyRequest represents the request structure for setting scheduling strategies -type SaveStrategyRequest struct { - Strategies []*domain.SchedulingStrategy `json:"strategies"` -} - -func (h *Handler) SaveSchedulingStrategiesHandler(w http.ResponseWriter, r *http.Request) { - var req SaveStrategyRequest - err := h.JSONBind(r, &req) - if err != nil { - h.ErrorResponse(w, http.StatusBadRequest, "Invalid request payload: "+err.Error()) - return - } - - err = h.Service.SaveSchedulingStrategy(r.Context(), req.Strategies) - if err != nil { - h.ErrorResponse(w, http.StatusInternalServerError, "Failed to save scheduling strategies: "+err.Error()) - return - } - h.SuccessResponse(w, "Scheduling strategies saved successfully") -} diff --git a/rest/token_hdl.go b/rest/token_hdl.go deleted file mode 100644 index 66cf3a4..0000000 --- a/rest/token_hdl.go +++ /dev/null @@ -1,46 +0,0 @@ -package rest - -import ( - "net/http" - "time" - - "github.com/Gthulhu/api/util" -) - -// TokenResponse represents the response structure for JWT token generation -type TokenResponse struct { - Success bool `json:"success"` - Message string `json:"message"` - Timestamp string `json:"timestamp"` - Token string `json:"token,omitempty"` -} - -// TokenRequest represents the request structure for JWT token generation -type TokenRequest struct { - PublicKey string `json:"public_key"` // PEM encoded public key -} - -// GenTokenHandler handles JWT token generation upon public key verification -func (h Handler) GenTokenHandler(w http.ResponseWriter, r *http.Request) { - var req TokenRequest - err := h.JSONBind(r, &req) - if err != nil { - h.ErrorResponse(w, http.StatusBadRequest, "Invalid JSON format: "+err.Error()) - return - } - token, err := h.Service.VerifyAndGenerateToken(r.Context(), req.PublicKey) - if err != nil { - h.ErrorResponse(w, http.StatusUnauthorized, "Public key verification failed: "+err.Error()) - return - } - - util.GetLogger().Debug("Generated JWT token for client") - - resp := TokenResponse{ - Success: true, - Message: "Token generated successfully", - Timestamp: time.Now().UTC().Format(time.RFC3339), - Token: token, - } - h.JSONResponse(w, http.StatusOK, resp) -} diff --git a/service/auth_svc.go b/service/auth_svc.go deleted file mode 100644 index a34b27f..0000000 --- a/service/auth_svc.go +++ /dev/null @@ -1,75 +0,0 @@ -package service - -import ( - "context" - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "fmt" - "time" - - "github.com/golang-jwt/jwt/v5" -) - -// VerifyAndGenerateToken verifies the provided public key and generates a JWT token if valid -func (svc *Service) VerifyAndGenerateToken(ctx context.Context, publicKey string) (string, error) { - err := svc.VerifyPublicKey(publicKey) - if err != nil { - return "", fmt.Errorf("public key verification failed: %v", err) - } - // Generate client ID from public key hash (simplified) - clientID := fmt.Sprintf("client_%d", time.Now().Unix()) - token, err := svc.generateJWT(clientID) - if err != nil { - return "", fmt.Errorf("JWT generation failed: %v", err) - } - return token, nil -} - -// verifyPublicKey verifies if the provided public key matches our private key -func (svc *Service) VerifyPublicKey(publicKeyPEM string) error { - block, _ := pem.Decode([]byte(publicKeyPEM)) - if block == nil { - return fmt.Errorf("failed to decode PEM block containing public key") - } - - publicKey, err := x509.ParsePKIXPublicKey(block.Bytes) - if err != nil { - return fmt.Errorf("failed to parse public key: %v", err) - } - - rsaPublicKey, ok := publicKey.(*rsa.PublicKey) - if !ok { - return fmt.Errorf("public key is not RSA") - } - - // Compare public key with our private key's public key - if !rsaPublicKey.Equal(&svc.jwtPrivateKey.PublicKey) { - return fmt.Errorf("public key does not match server's private key") - } - - return nil -} - -// generateJWT generates a JWT token for authenticated client -func (svc *Service) generateJWT(clientID string) (string, error) { - claims := Claims{ - ClientID: clientID, - RegisteredClaims: jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(svc.config.JWT.TokenDuration) * time.Hour)), - IssuedAt: jwt.NewNumericDate(time.Now()), - NotBefore: jwt.NewNumericDate(time.Now()), - Issuer: "bss-api-server", - Subject: clientID, - }, - } - - token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) - return token.SignedString(svc.jwtPrivateKey) -} - -// Claims represents JWT token claims -type Claims struct { - ClientID string `json:"client_id"` - jwt.RegisteredClaims -} diff --git a/service/auth_svc_test.go b/service/auth_svc_test.go deleted file mode 100644 index 1e17d48..0000000 --- a/service/auth_svc_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package service_test - -import ( - "context" - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "path/filepath" - "runtime" - "testing" - - "github.com/Gthulhu/api/config" - "github.com/Gthulhu/api/service" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestVerifyJWTToken(t *testing.T) { - _, f, _, _ := runtime.Caller(0) - configPath := filepath.Join(filepath.Dir(f), "..", "config", "jwt_private_key.key") - - privateKey, err := config.InitJWTRsaKey(config.JWTConfig{ - PrivateKeyPath: configPath, - }) - require.NoError(t, err, "can't init jwt rsa key") - - svc, err := service.NewService(context.Background(), service.Params{ - Config: &config.Config{ - Strategies: config.StrategiesConfig{}, - }, - JWTPrivateKey: privateKey, - }) - require.NoError(t, err, "new service failed") - - pubKeyString, err := PublicKeyToString(&privateKey.PublicKey) - require.NoError(t, err, "generate public key string failed") - - token, err := svc.VerifyAndGenerateToken(context.Background(), pubKeyString) - require.NoError(t, err, "verify public key and generate token failed") - assert.NotEmpty(t, token) -} - -func PublicKeyToString(pub *rsa.PublicKey) (string, error) { - pubASN1, err := x509.MarshalPKIXPublicKey(pub) - if err != nil { - return "", err - } - - pubPEM := pem.EncodeToMemory(&pem.Block{ - Type: "PUBLIC KEY", - Bytes: pubASN1, - }) - - return string(pubPEM), nil -} diff --git a/service/metrics_svc.go b/service/metrics_svc.go deleted file mode 100644 index deea026..0000000 --- a/service/metrics_svc.go +++ /dev/null @@ -1,32 +0,0 @@ -package service - -import ( - "context" - "errors" - "sync/atomic" - - "github.com/Gthulhu/api/domain" -) - -var ( - latestBssData atomic.Value - ErrNoBssData = errors.New("no BSS metrics data available") -) - -// SaveBSSMetrics saves the provided BSS metrics data -func (svc *Service) SaveBSSMetrics(ctx context.Context, bssMetrics *domain.BssData) error { - latestBssData.Store(bssMetrics) - return nil -} - -// GetBSSMetrics retrieves the latest BSS metrics data -func (svc *Service) GetBSSMetrics(ctx context.Context) (*domain.BssData, error) { - data := latestBssData.Load() - if data != nil { - bssData, ok := data.(*domain.BssData) - if ok { - return bssData, nil - } - } - return &domain.BssData{}, ErrNoBssData -} diff --git a/service/pods_svc.go b/service/pods_svc.go deleted file mode 100644 index 2d0c050..0000000 --- a/service/pods_svc.go +++ /dev/null @@ -1,162 +0,0 @@ -package service - -import ( - "bufio" - "context" - "fmt" - "log/slog" - "os" - "strconv" - "strings" - - "github.com/Gthulhu/api/domain" - "github.com/Gthulhu/api/util" -) - -const ( - procDir = "/proc" -) - -// GetAllPodInfos retrieves all pod information by scanning the /proc filesystem -func (svc *Service) GetAllPodInfos(ctx context.Context) ([]*domain.PodInfo, error) { - return svc.FindPodInfoFrom(ctx, procDir) -} - -// FindPodInfoFrom scans the given rootDir (e.g., /proc) to find pod information -func (svc *Service) FindPodInfoFrom(ctx context.Context, rootDir string) ([]*domain.PodInfo, error) { - podMap := make(map[string]*domain.PodInfo) - - // Walk through /proc to find all processes - entries, err := os.ReadDir(rootDir) - if err != nil { - return nil, fmt.Errorf("failed to read /proc directory: %v", err) - } - - for _, entry := range entries { - if !entry.IsDir() { - continue - } - - // Check if directory name is a PID (numeric) - pid, err := strconv.Atoi(entry.Name()) - if err != nil { - // Not a numeric PID directory (e.g., "acpi", "bus", etc.) — skip - continue - } - - // Read cgroup information for this process - cgroupPath := fmt.Sprintf("%s/%d/cgroup", rootDir, pid) - file, err := os.Open(cgroupPath) - if err != nil { - util.GetLogger().Error("failed to open cgroup file", slog.String("path", cgroupPath), util.LogErrAttr(err)) - continue - } - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - if strings.Contains(line, "kubepods") { - err = svc.parseCgroupToPodInfo(rootDir, line, pid, podMap) - if err != nil { - // If we fail to parse for this process, log and continue with others - util.GetLogger().Error("failed to parse cgroup to pod info", slog.String("line", line), util.LogErrAttr(err)) - break - } - } - } - if err := scanner.Err(); err != nil { - util.GetLogger().Error("scanner error reading cgroup file", slog.String("path", cgroupPath), util.LogErrAttr(err)) - } - _ = file.Close() - } - - // Convert map to slice - var pods []*domain.PodInfo - for _, podInfo := range podMap { - pods = append(pods, podInfo) - } - - return pods, nil -} - -// parseCgroupToPodInfo parses a cgroup line (e.g 9:cpuset:/kubepods/burstable/pod123abc-456def/docker-abcdef.scope) to extract pod info and updates the podInfoMap -func (svc *Service) parseCgroupToPodInfo(rootDir string, line string, pid int, podInfoMap map[string]*domain.PodInfo) error { - parts := strings.Split(line, ":") - if len(parts) >= 3 { - cgroupHierarchy := parts[2] - - // Extract pod information - podUID, containerID, err := svc.getPodInfoFromCgroup(cgroupHierarchy) - if err != nil { - return err - } - - // Get process information - process, err := svc.getProcessInfo(rootDir, pid) - if err != nil { - return err - } - - // Create or update pod info - if podInfo, exists := podInfoMap[podUID]; exists { - podInfo.Processes = append(podInfo.Processes, process) - if containerID != "" && podInfo.ContainerID == "" { - podInfo.ContainerID = containerID - } - } else { - podInfoMap[podUID] = &domain.PodInfo{ - PodUID: podUID, - ContainerID: containerID, - Processes: []domain.PodProcess{process}, - } - } - } - return nil -} - -// getPodInfoFromCgroup extracts pod information from cgroup path -func (svc *Service) getPodInfoFromCgroup(cgroupPath string) (podUID string, containerID string, err error) { - // Parse cgroup path to extract pod information - // Format: /kubepods/burstable/pod/ - // or: /kubepods/pod/ - parts := strings.Split(cgroupPath, "/") - for i, part := range parts { - if strings.HasPrefix(part, "pod") { - podUID = strings.TrimPrefix(part, "pod") - podUID = strings.ReplaceAll(podUID, "_", "-") - if i+1 < len(parts) { - containerID = parts[i+1] - } - break - } - } - - if podUID == "" { - return "", "", fmt.Errorf("pod UID not found in cgroup path") - } - - return podUID, containerID, nil -} - -// getProcessInfo reads process information from /proc// -func (svc *Service) getProcessInfo(rootDir string, pid int) (domain.PodProcess, error) { - process := domain.PodProcess{PID: pid} - - // Read command from /proc//comm - commPath := fmt.Sprintf("/%s/%d/comm", rootDir, pid) - if data, err := os.ReadFile(commPath); err == nil { - process.Command = strings.TrimSpace(string(data)) - } - - // Read PPID from /proc//stat - statPath := fmt.Sprintf("/%s/%d/stat", rootDir, pid) - if data, err := os.ReadFile(statPath); err == nil { - fields := strings.Fields(string(data)) - if len(fields) >= 4 { - if ppid, err := strconv.Atoi(fields[3]); err == nil { - process.PPID = ppid - } - } - } - - return process, nil -} diff --git a/service/pods_svc_test.go b/service/pods_svc_test.go deleted file mode 100644 index 640fd61..0000000 --- a/service/pods_svc_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package service_test - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/Gthulhu/api/service" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// setupFakeProcDir creates a temporary fake /proc directory structure for testing -func setupFakeProcDir(t *testing.T) string { - root := t.TempDir() - // fake pid 1234 - pidDir := filepath.Join(root, "1234") - if err := os.Mkdir(pidDir, 0755); err != nil { - t.Fatal(err) - } - - // cgroup - cgroupContent := "9:cpuset:/kubepods/burstable/pod123abc-456def/docker-abcdef.scope\n" - if err := os.WriteFile(filepath.Join(pidDir, "cgroup"), []byte(cgroupContent), 0644); err != nil { - t.Fatal(err) - } - - // comm - if err := os.WriteFile(filepath.Join(pidDir, "comm"), []byte("nginx\n"), 0644); err != nil { - t.Fatal(err) - } - - // stat - statLine := "1234 (nginx) S 1 2 3 4 5 0 0 0 0 0 0 0 0 0 0 0 0 0 0" - if err := os.WriteFile(filepath.Join(pidDir, "stat"), []byte(statLine), 0644); err != nil { - t.Fatal(err) - } - - return root -} - -// TestFindPodInfoFrom tests the FindPodInfoFrom function -func TestFindPodInfoFrom(t *testing.T) { - fakeProc := setupFakeProcDir(t) - svc := &service.Service{} - - pods, err := svc.FindPodInfoFrom(context.Background(), fakeProc) - assert.NoError(t, err, "FindPodInfoFrom should not return error") - require.Len(t, pods, 1, "should find one pod info") - - p := pods[0] - assert.EqualValues(t, p.PodUID, "123abc-456def", "unexpected podUID") - require.Len(t, p.Processes, 1, "should have one process") - assert.EqualValues(t, p.Processes[0].Command, "nginx", "unexpected command") -} diff --git a/service/strategies_svc.go b/service/strategies_svc.go deleted file mode 100644 index ffcdeda..0000000 --- a/service/strategies_svc.go +++ /dev/null @@ -1,131 +0,0 @@ -package service - -import ( - "context" - "fmt" - "regexp" - "sync/atomic" - - "github.com/Gthulhu/api/cache" - "github.com/Gthulhu/api/domain" - "github.com/Gthulhu/api/util" -) - -var ( - latestSchedulingStrategyData atomic.Value -) - -// SaveSchedulingStrategy saves the provided scheduling strategies and invalidates the cache -func (svc *Service) SaveSchedulingStrategy(ctx context.Context, strategy []*domain.SchedulingStrategy) error { - latestSchedulingStrategyData.Store(strategy) - svc.StrategyCache.Invalidate() - return nil -} - -// FindCurrentUsingSchedulingStrategies finds the current scheduling strategies being used -func (svc *Service) FindCurrentUsingSchedulingStrategiesWithPID(ctx context.Context) ([]*domain.SchedulingStrategy, bool, error) { - data := latestSchedulingStrategyData.Load() - if data != nil { - strategies, ok := data.([]*domain.SchedulingStrategy) - if ok { - return svc.FindSchedulingStrategiesWithPID(ctx, procDir, strategies) - } - } - - return []*domain.SchedulingStrategy{}, false, nil -} - -// GetStrategyCacheStats returns statistics about the strategy cache -func (svc *Service) GetStrategyCacheStats() map[string]any { - stats := svc.StrategyCache.GetStats() - return stats -} - -// FindSchedulingStrategiesWithPID finds scheduling strategies with associated PIDs -func (svc *Service) FindSchedulingStrategiesWithPID(ctx context.Context, rootDir string, usingStrategies []*domain.SchedulingStrategy) ([]*domain.SchedulingStrategy, bool, error) { - cachedStrategies := svc.StrategyCache.GetStrategiesQuick(usingStrategies) - if cachedStrategies != nil { - return cachedStrategies, true, nil - } - - // Recalculate strategies - pods, err := svc.FindPodInfoFrom(ctx, rootDir) - if err != nil { - return nil, false, fmt.Errorf("failed to get pod-pid mappings: %v", err) - } - - var finalStrategies []*domain.SchedulingStrategy - for _, strategy := range usingStrategies { - if len(strategy.Selectors) > 0 { - matchedPIDs, err := svc.findPIDsByStrategy(ctx, pods, strategy) - if err != nil { - util.GetLogger().Error("Error finding PIDs for strategy", util.LogErrAttr(err)) - continue - } - - for _, pid := range matchedPIDs { - finalStrategies = append(finalStrategies, &domain.SchedulingStrategy{ - Priority: strategy.Priority, - ExecutionTime: strategy.ExecutionTime, - Selectors: strategy.Selectors, - PID: pid, - }) - } - } else if strategy.PID != 0 { - finalStrategies = append(finalStrategies, strategy) - } - } - // Update cache with both pod and strategy snapshots - svc.StrategyCache.UpdatePodSnapshot(pods) - svc.StrategyCache.UpdateStrategySnapshot(usingStrategies) - svc.StrategyCache.SetStrategies(finalStrategies) - return finalStrategies, false, nil -} - -// findPIDsByStrategy finds PIDs that match the given scheduling strategy -func (svc *Service) findPIDsByStrategy(ctx context.Context, pods []*domain.PodInfo, strategy *domain.SchedulingStrategy) ([]int, error) { - var matchedPIDs []int - - // Set default regex if empty - if strategy.CommandRegex == "" { - strategy.CommandRegex = ".*" - } - - // Compile regex and add to cache - compiledRegex, err := regexp.Compile(strategy.CommandRegex) - if err != nil { - return nil, fmt.Errorf("invalid regex pattern '%s': %v", strategy.CommandRegex, err) - } - - for _, pod := range pods { - podSpec, ok := cache.GetKubernetesPod(pod.PodUID) - if !ok { - podSpecTemp, err := svc.K8sAdapter.GetPodByPodUID(ctx, pod.PodUID) - if err != nil { - return nil, err - } - podSpec = podSpecTemp - cache.SetKubernetesPodCache(pod.PodUID, podSpec) - } - labels := podSpec.Labels - matches := true - for _, selector := range strategy.Selectors { - value, exists := labels[selector.Key] - if !exists || value != selector.Value { - matches = false - break - } - } - - if matches { - // Use cached regex for all process matching - for _, process := range pod.Processes { - if compiledRegex.MatchString(process.Command) { - matchedPIDs = append(matchedPIDs, process.PID) - } - } - } - } - - return matchedPIDs, nil -} diff --git a/service/strategies_svc_test.go b/service/strategies_svc_test.go deleted file mode 100644 index 09447b4..0000000 --- a/service/strategies_svc_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package service_test - -import ( - "context" - "testing" - - "github.com/Gthulhu/api/adapter/kubernetes" - "github.com/Gthulhu/api/cache" - "github.com/Gthulhu/api/domain" - "github.com/Gthulhu/api/service" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// TestFindSchedulingStrategiesWithPID tests the FindSchedulingStrategiesWithPID function -func TestFindSchedulingStrategiesWithPID(t *testing.T) { - fakeProc := setupFakeProcDir(t) - - mockK8SAdapter := kubernetes.NewMockK8sAdapter(t) - svc := &service.Service{ - StrategyCache: cache.NewStrategyCache(), - K8sAdapter: mockK8SAdapter, - } - - strategies := []*domain.SchedulingStrategy{{ - Priority: true, - ExecutionTime: 1, - Selectors: []domain.LabelSelector{ - { - Key: "test", - Value: "test", - }, - }, - }} - - mockK8SAdapter.EXPECT().GetPodByPodUID(mock.Anything, "123abc-456def").Return(v1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - "test": "test", - }, - }, - }, nil) - res, fromCache, err := svc.FindSchedulingStrategiesWithPID(context.Background(), fakeProc, strategies) - require.False(t, fromCache, "result should not be from cache") - require.NoError(t, err, "FindSchedulingStrategiesWithPID should not return error") - require.Len(t, res, 1, "should find one scheduling strategy") - require.EqualValues(t, 1234, res[0].PID, "unexpected PID in scheduling strategy") - - res, fromCache, err = svc.FindSchedulingStrategiesWithPID(context.Background(), fakeProc, strategies) - require.True(t, fromCache, "result should not be from cache") - require.NoError(t, err, "FindSchedulingStrategiesWithPID should not return error") - require.Len(t, res, 1, "should find one scheduling strategy") - require.EqualValues(t, 1234, res[0].PID, "unexpected PID in scheduling strategy") -} diff --git a/service/svc.go b/service/svc.go deleted file mode 100644 index d06e5f5..0000000 --- a/service/svc.go +++ /dev/null @@ -1,63 +0,0 @@ -package service - -import ( - "context" - "crypto/rsa" - - "github.com/Gthulhu/api/adapter/kubernetes" - "github.com/Gthulhu/api/cache" - "github.com/Gthulhu/api/config" - "github.com/Gthulhu/api/domain" -) - -// Params holds the parameters for creating a new Service -type Params struct { - K8sAdapter kubernetes.K8sAdapter - StrategyCache *cache.StrategyCache - JWTPrivateKey *rsa.PrivateKey - Config *config.Config -} - -// NewService creates a new Service instance -func NewService(ctx context.Context, params Params) (*Service, error) { - svc := &Service{ - K8sAdapter: params.K8sAdapter, - StrategyCache: params.StrategyCache, - jwtPrivateKey: params.JWTPrivateKey, - config: params.Config, - } - - if len(params.Config.Strategies.Default) > 0 { - defaultStrategies := []*domain.SchedulingStrategy{} - for _, strat := range params.Config.Strategies.Default { - ds := domain.SchedulingStrategy{ - Priority: strat.Priority, - ExecutionTime: strat.ExecutionTime, - PID: strat.PID, - CommandRegex: strat.CommandRegex, - Selectors: []domain.LabelSelector{}, - } - for _, sel := range strat.Selectors { - ds.Selectors = append(ds.Selectors, domain.LabelSelector{ - Key: sel.Key, - Value: sel.Value, - }) - } - defaultStrategies = append(defaultStrategies, &ds) - } - err := svc.SaveSchedulingStrategy(context.Background(), defaultStrategies) - if err != nil { - return nil, err - } - } - - return svc, nil -} - -// Service represents the main service structure -type Service struct { - kubernetes.K8sAdapter - *cache.StrategyCache - jwtPrivateKey *rsa.PrivateKey - config *config.Config -} diff --git a/util/logger.go b/util/logger.go deleted file mode 100644 index 872ac99..0000000 --- a/util/logger.go +++ /dev/null @@ -1,52 +0,0 @@ -package util - -import ( - "context" - "log/slog" - "os" - - "github.com/pkg/errors" - "github.com/rs/zerolog" -) - -var logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) - -var ( - loggerCtxKey = struct{}{} -) - -func AddLoggerToCtx(ctx context.Context, logger *slog.Logger) context.Context { - return context.WithValue(ctx, loggerCtxKey, logger) -} - -func LoggerFromCtx(ctx context.Context) *slog.Logger { - logger, ok := ctx.Value(loggerCtxKey).(*slog.Logger) - if !ok || logger == nil { - return GetLogger() - } - return logger -} - -func GetLogger() *slog.Logger { - return logger -} - -func LogErrAttr(err error) slog.Attr { - return slog.String("error", errors.WithStack(err).Error()) -} - -func InitLogger() *zerolog.Logger { - consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: "15:04:05"} - - logger := zerolog.New(consoleWriter). - With(). - Timestamp(). - Logger() - zerolog.SetGlobalLevel(zerolog.DebugLevel) - zerolog.DefaultContextLogger = &logger - return &logger -} - -func Logger(ctx context.Context) *zerolog.Logger { - return zerolog.Ctx(ctx) -} diff --git a/static/app.js b/web/static/app.js similarity index 100% rename from static/app.js rename to web/static/app.js diff --git a/static/index.html b/web/static/index.html similarity index 100% rename from static/index.html rename to web/static/index.html diff --git a/static/style.css b/web/static/style.css similarity index 100% rename from static/style.css rename to web/static/style.css From 233a5390d79f1a9b65cad3664cb38eb2d27bc647 Mon Sep 17 00:00:00 2001 From: Vic Xu Date: Tue, 25 Nov 2025 15:53:58 +0800 Subject: [PATCH 07/18] account/rbac: add dockertest --- config/manager_config.default.toml | 5 +- config/manager_config.go | 5 +- config/manager_config.test.toml | 5 +- deployment/local/docker-compose.infra.yaml | 3 +- go.mod | 25 ++++- go.sum | 80 ++++++++++++++- manager/app/module.go | 47 +++++---- manager/app/rest_app.go | 18 +++- manager/repository/repo.go | 10 +- manager/rest/handler_test.go | 22 ++++- pkg/container/container.go | 110 +++++++++++++++++++++ pkg/container/mongo_container.go | 98 ++++++++++++++++++ 12 files changed, 395 insertions(+), 33 deletions(-) create mode 100644 pkg/container/container.go create mode 100644 pkg/container/mongo_container.go diff --git a/config/manager_config.default.toml b/config/manager_config.default.toml index 8d227d5..b425453 100644 --- a/config/manager_config.default.toml +++ b/config/manager_config.default.toml @@ -7,7 +7,10 @@ level = "info" path = "logs/app.log" [mongodb] -uri = "mongodb://test:test@localhost:27017/?tls=false" +host = "localhost" +port = "27017" +user = "test" +password = "test" database = "appdb" ca_pem = """ -----BEGIN CERTIFICATE----- diff --git a/config/manager_config.go b/config/manager_config.go index 8f463ed..9fa01e1 100644 --- a/config/manager_config.go +++ b/config/manager_config.go @@ -27,9 +27,12 @@ type ManageConfig struct { } type MongoDBConfig struct { - URI string `mapstructure:"uri"` Database string `mapstructure:"database"` CAPem string `mapstructure:"ca_pem"` + User string `mapstructure:"user"` + Password string `mapstructure:"password"` + Port string `mapstructure:"port"` + Host string `mapstructure:"host"` } type KeyConfig struct { diff --git a/config/manager_config.test.toml b/config/manager_config.test.toml index 442ff83..2a6cdb7 100644 --- a/config/manager_config.test.toml +++ b/config/manager_config.test.toml @@ -8,7 +8,10 @@ level = "info" path = "logs/app.log" [mongodb] -uri = "mongodb://test:test@localhost:27017/?tls=false" +host = "localhost" +port = "27017" +user = "test" +password = "test" database = "appdb" [key] diff --git a/deployment/local/docker-compose.infra.yaml b/deployment/local/docker-compose.infra.yaml index c786314..6b49f61 100644 --- a/deployment/local/docker-compose.infra.yaml +++ b/deployment/local/docker-compose.infra.yaml @@ -1,9 +1,8 @@ services: mongo: container_name: mongo - image: mongo:4.4.27 + image: mongo:8.2.2 environment: - MONGODB_CLIENT_EXTRA_FLAGS: "--authenticationDatabase admin" MONGO_INITDB_ROOT_USERNAME: test MONGO_INITDB_ROOT_PASSWORD: test ports: diff --git a/go.mod b/go.mod index a137420..23ff4a8 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ tool github.com/vektra/mockery/v3 require ( github.com/golang-jwt/jwt/v5 v5.3.0 github.com/labstack/echo/v4 v4.13.4 - github.com/pkg/errors v0.9.1 + github.com/ory/dockertest/v3 v3.12.0 github.com/rs/zerolog v1.33.0 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.21.0 @@ -20,13 +20,25 @@ require ( ) require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/brunoga/deep v1.2.4 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/containerd/continuity v0.4.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/docker/cli v27.4.1+incompatible // indirect + github.com/docker/docker v27.1.1+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jedib0t/go-pretty/v6 v6.6.7 // indirect @@ -44,11 +56,19 @@ require ( github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/sys/user v0.3.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/opencontainers/runc v1.2.3 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect @@ -60,7 +80,7 @@ require ( github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect @@ -77,5 +97,6 @@ require ( golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/tools v0.38.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 55e4e9b..0cba427 100644 --- a/go.sum +++ b/go.sum @@ -1,31 +1,64 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/brunoga/deep v1.2.4 h1:Aj9E9oUbE+ccbyh35VC/NHlzzjfIVU69BXu2mt2LmL8= github.com/brunoga/deep v1.2.4/go.mod h1:GDV6dnXqn80ezsLSZ5Wlv1PdKAWAO4L5PnKYtv2dgaI= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= +github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI= +github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= +github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +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/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +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/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= @@ -50,6 +83,8 @@ github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcX github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= @@ -63,6 +98,20 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80= +github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM= +github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw= +github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -81,6 +130,8 @@ github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWR github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= @@ -98,6 +149,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -114,14 +166,17 @@ github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver/v2 v2.4.0 h1:Oq6BmUAAFTzMeh6AonuDlgZMuAuEiUxoAD1koK5MuFo= go.mongodb.org/mongo-driver/v2 v2.4.0/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI= @@ -138,27 +193,40 @@ go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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-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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -177,12 +245,22 @@ golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +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= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/manager/app/module.go b/manager/app/module.go index 409189c..a02ddbf 100644 --- a/manager/app/module.go +++ b/manager/app/module.go @@ -5,15 +5,12 @@ import ( "github.com/Gthulhu/api/manager/repository" "github.com/Gthulhu/api/manager/rest" "github.com/Gthulhu/api/manager/service" + "github.com/Gthulhu/api/pkg/container" "go.uber.org/fx" ) -func ConfigModule(configName string, configPath string) (fx.Option, error) { - cfg, err := config.InitManagerConfig(configName, configPath) - if err != nil { - return nil, err - } - +// ConfigModule creates an Fx module that provides configuration structs +func ConfigModule(cfg config.ManageConfig) (fx.Option, error) { return fx.Options( fx.Provide(func() config.ManageConfig { return cfg @@ -34,8 +31,8 @@ func ConfigModule(configName string, configPath string) (fx.Option, error) { } // RepoModule creates an Fx module that provides the repository layer, return repository.Repository -func RepoModule(configName string, configPath string) (fx.Option, error) { - configModule, err := ConfigModule(configName, configPath) +func RepoModule(cfg config.ManageConfig) (fx.Option, error) { + configModule, err := ConfigModule(cfg) if err != nil { return nil, err } @@ -47,12 +44,7 @@ func RepoModule(configName string, configPath string) (fx.Option, error) { } // ServiceModule creates an Fx module that provides the service layer, return domain.Service -func ServiceModule(configName string, configPath string) (fx.Option, error) { - repoModule, err := RepoModule(configName, configPath) - if err != nil { - return nil, err - } - +func ServiceModule(repoModule fx.Option) (fx.Option, error) { return fx.Options( repoModule, fx.Provide(service.NewService), @@ -60,14 +52,31 @@ func ServiceModule(configName string, configPath string) (fx.Option, error) { } // HandlerModule creates an Fx module that provides the REST handler, return *rest.Handler -func HandlerModule(configName string, configPath string) (fx.Option, error) { - serviceModule, err := ServiceModule(configName, configPath) +func HandlerModule(serviceModule fx.Option) (fx.Option, error) { + return fx.Options( + serviceModule, + fx.Provide(rest.NewHandler), + ), nil +} + +// TestRepoModule creates an Fx module that provides the repository layer for testing, return repository.Repository +func TestRepoModule(cfg config.ManageConfig, containerBuilder *container.ContainerBuilder) (fx.Option, error) { + configModule, err := ConfigModule(cfg) + if err != nil { + return nil, err + } + _, err = container.RunMongoContainer(containerBuilder, "api_test_mongo", container.MongoContainerConnection{ + Username: cfg.MongoDB.User, + Password: cfg.MongoDB.Password, + Database: cfg.MongoDB.Database, + Port: cfg.MongoDB.Port, + Host: cfg.MongoDB.Host, + }) if err != nil { return nil, err } - return fx.Options( - serviceModule, - fx.Provide(rest.NewHandler), + configModule, + fx.Provide(repository.NewRepository), ), nil } diff --git a/manager/app/rest_app.go b/manager/app/rest_app.go index 3c489bb..3dc2f59 100644 --- a/manager/app/rest_app.go +++ b/manager/app/rest_app.go @@ -13,7 +13,23 @@ import ( ) func NewRestApp(configName string, configDirPath string) (*fx.App, error) { - handlerModule, err := HandlerModule(configName, configDirPath) + + cfg, err := config.InitManagerConfig(configName, configDirPath) + if err != nil { + return nil, err + } + + repoModule, err := RepoModule(cfg) + if err != nil { + return nil, err + } + + serviceModule, err := ServiceModule(repoModule) + if err != nil { + return nil, err + } + + handlerModule, err := HandlerModule(serviceModule) if err != nil { return nil, err } diff --git a/manager/repository/repo.go b/manager/repository/repo.go index 9216ec9..d5e848c 100644 --- a/manager/repository/repo.go +++ b/manager/repository/repo.go @@ -23,10 +23,12 @@ type Params struct { } func NewRepository(params Params) (domain.Repository, error) { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() - mongoOpts := options.Client().ApplyURI(params.MongoConfig.URI) + uri := fmt.Sprintf("mongodb://%s:%s@%s:%s", params.MongoConfig.User, params.MongoConfig.Password, params.MongoConfig.Host, params.MongoConfig.Port) + + mongoOpts := options.Client().ApplyURI(uri) if params.MongoConfig.CAPem != "" { caPool := x509.NewCertPool() caPool.AppendCertsFromPEM([]byte(params.MongoConfig.CAPem)) @@ -40,11 +42,11 @@ func NewRepository(params Params) (domain.Repository, error) { client, err := mongo.Connect() if err != nil { - return nil, fmt.Errorf("connect to mongodb: %w", err) + return nil, fmt.Errorf("connect to mongodb: %w, uri:%s", err, uri) } if err := client.Ping(ctx, nil); err != nil { - return nil, fmt.Errorf("ping mongodb: %w", err) + return nil, fmt.Errorf("ping mongodb: %w, uri:%s", err, uri) } dbName := params.MongoConfig.Database diff --git a/manager/rest/handler_test.go b/manager/rest/handler_test.go index 669a7a6..8307377 100644 --- a/manager/rest/handler_test.go +++ b/manager/rest/handler_test.go @@ -10,6 +10,7 @@ import ( "github.com/Gthulhu/api/config" "github.com/Gthulhu/api/manager/app" "github.com/Gthulhu/api/manager/rest" + "github.com/Gthulhu/api/pkg/container" "github.com/labstack/echo/v4" "github.com/stretchr/testify/suite" "go.uber.org/fx" @@ -24,11 +25,25 @@ type HandlerTestSuite struct { Handler *rest.Handler Ctx context.Context Engine *echo.Echo + *container.ContainerBuilder } func (suite *HandlerTestSuite) SetupSuite() { suite.Ctx = context.Background() - handlerModule, err := app.HandlerModule("manager_config.test.toml", config.GetAbsPath("config")) + containerBuilder, err := container.NewContainerBuilder("") + suite.Require().NoError(err, "Failed to create container builder") + suite.ContainerBuilder = containerBuilder + + cfg, err := config.InitManagerConfig("manager_config.test.toml", config.GetAbsPath("config")) + suite.Require().NoError(err, "Failed to initialize manager config") + + repoModule, err := app.TestRepoModule(cfg, suite.ContainerBuilder) + suite.Require().NoError(err, "Failed to create repo module") + + serviceModule, err := app.ServiceModule(repoModule) + suite.Require().NoError(err, "Failed to create service module") + + handlerModule, err := app.HandlerModule(serviceModule) suite.Require().NoError(err, "Failed to create handler module") opt := fx.Options( handlerModule, @@ -45,6 +60,11 @@ func (suite *HandlerTestSuite) SetupSuite() { suite.Handler.SetupRoutes(e) } +func (suite *HandlerTestSuite) TearDownSuite() { + err := suite.ContainerBuilder.PruneAll() + suite.Require().NoError(err, "Failed to terminate containers") +} + func (suite *HandlerTestSuite) JSONDecode(r *httptest.ResponseRecorder, dst any) { decoder := json.NewDecoder(r.Body) err := decoder.Decode(dst) diff --git a/pkg/container/container.go b/pkg/container/container.go new file mode 100644 index 0000000..7bc3379 --- /dev/null +++ b/pkg/container/container.go @@ -0,0 +1,110 @@ +package container + +import ( + "context" + "errors" + "time" + + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" +) + +const ( + containerRunningState = "running" + containerExitedState = "exited" + containerPausedState = "paused" + containerCreatedState = "created" +) + +// NewContainerBuilder creates a new ContainerBuilder instance. The endpoint parameter specifies the Docker endpoint to connect to. can be empty to use the default. +func NewContainerBuilder(endpoint string) (*ContainerBuilder, error) { + pool, err := dockertest.NewPool(endpoint) + if err != nil { + return nil, err + } + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + err = pool.Client.PingWithContext(ctx) + if err != nil { + return nil, errors.New("could not connect to docker endpoint: " + err.Error()) + } + return &ContainerBuilder{ + Pool: pool, + containerIDs: make(map[string]ContainerInfo), + }, nil +} + +type ContainerType string + +const ( + ContainerTypeMongoDB ContainerType = "mongodb" +) + +type ContainerInfo struct { + Name string + Type ContainerType +} + +type ContainerBuilder struct { + *dockertest.Pool + containerIDs map[string]ContainerInfo +} + +func (builder *ContainerBuilder) RemoveByID(containerID string) error { + return builder.Client.RemoveContainer(docker.RemoveContainerOptions{ID: containerID, Force: true, RemoveVolumes: true}) +} + +func (builder *ContainerBuilder) FindContainer(containerName string) (*docker.APIContainers, error) { + containers, err := builder.Client.ListContainers(docker.ListContainersOptions{ + All: true, + Filters: map[string][]string{ + "name": {containerName}, + }, + }) + if err != nil { + return nil, err + } + if len(containers) == 0 { + return nil, nil + } + if containers[0].State != containerRunningState { + if err := builder.RemoveByID(containers[0].ID); err != nil { + return nil, err + } + return nil, nil + } + + return &containers[0], nil +} + +func (builder *ContainerBuilder) PruneAll() error { + var err error + for id := range builder.containerIDs { + purErr := builder.RemoveByID(id) + if purErr != nil { + if err == nil { + err = purErr + } else { + err = errors.Join(err, purErr) + } + } + } + return err +} + +func (builder *ContainerBuilder) AllContainerIDs() []string { + ids := make([]string, 0, len(builder.containerIDs)) + for id := range builder.containerIDs { + ids = append(ids, id) + } + return ids +} + +func (builder *ContainerBuilder) AddContainer(id string, containerInfo ContainerInfo) { + builder.containerIDs[id] = containerInfo +} + +func (builder *ContainerBuilder) GetContainerInfo(id string) (ContainerInfo, bool) { + info, ok := builder.containerIDs[id] + return info, ok +} diff --git a/pkg/container/mongo_container.go b/pkg/container/mongo_container.go new file mode 100644 index 0000000..9d4d943 --- /dev/null +++ b/pkg/container/mongo_container.go @@ -0,0 +1,98 @@ +package container + +import ( + "fmt" + "strconv" + + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" +) + +type MongoContainerOptions struct { + Username string + Password string + Database string + Port string +} + +type MongoContainerConnection struct { + Host string + Port string + Username string + Password string + Database string +} + +const ( + mongoDBPort = 27017 +) + +// RunMongoContainer runs a MongoDB container with the specified options and returns the connection details. +func RunMongoContainer(builder *ContainerBuilder, name string, options MongoContainerConnection) (MongoContainerConnection, error) { + runOptions := dockertest.RunOptions{ + Name: name, + Repository: "mongo", + Tag: "8.2.2", + Env: []string{ + "MONGO_INITDB_ROOT_USERNAME=" + options.Username, + "MONGO_INITDB_ROOT_PASSWORD=" + options.Password, + }, + } + if options.Database != "" { + runOptions.Env = append(runOptions.Env, "MONGO_INITDB_DATABASE="+options.Database) + } + if options.Port != "" { + runOptions.PortBindings = map[docker.Port][]docker.PortBinding{ + docker.Port(strconv.Itoa(mongoDBPort) + "/tcp"): {{HostIP: "127.0.0.1", HostPort: options.Port}}, + } + } + + container, err := builder.FindContainer(name) + if err != nil { + return MongoContainerConnection{}, err + } + if container != nil && container.State == "running" { + publicPort := int64(0) + host := "" + for _, bind := range container.Ports { + if bind.PrivatePort == mongoDBPort { + host = bind.IP + publicPort = bind.PublicPort + break + } + } + if publicPort == 0 { + return MongoContainerConnection{}, fmt.Errorf("failed to find public port for mongo container (%s)", name) + } + + builder.AddContainer(container.ID, ContainerInfo{ + Name: name, + Type: ContainerTypeMongoDB, + }) + return MongoContainerConnection{ + Host: host, + Port: strconv.FormatInt(publicPort, 10), + Username: options.Username, + Password: options.Password, + Database: options.Database, + }, nil + } + + resource, err := builder.RunWithOptions(&runOptions) + if err != nil { + return MongoContainerConnection{}, err + } + builder.AddContainer(resource.Container.ID, ContainerInfo{ + Name: name, + Type: ContainerTypeMongoDB, + }) + host := resource.GetBoundIP(strconv.Itoa(mongoDBPort) + "/tcp") + mongoPort := resource.GetPort(strconv.Itoa(mongoDBPort) + "/tcp") + return MongoContainerConnection{ + Host: host, + Port: mongoPort, + Username: options.Username, + Password: options.Password, + Database: options.Database, + }, nil +} From 01fccd8e93a4421ddfd66b12f051632d8934d8aa Mon Sep 17 00:00:00 2001 From: Vic Xu Date: Sun, 30 Nov 2025 18:19:13 +0800 Subject: [PATCH 08/18] account/rbac: implement role and auth handlers, and add handler integration test --- Makefile | 9 +- config/manager_config.default.toml | 2 +- config/manager_config.go | 34 ++- config/manager_config.test.toml | 3 +- go.mod | 8 +- go.sum | 37 ++- manager/app/module.go | 2 +- manager/domain/account.go | 57 ++-- manager/domain/auth.go | 13 +- manager/domain/enums.go | 19 ++ manager/domain/interface.go | 32 +-- manager/domain/value.go | 31 ++- manager/errs/errors.go | 34 +++ .../migration/001_init_collections.down.json | 5 + .../migration/001_init_collections.up.json | 182 +++++++++++++ .../002_init_default_permissions.up.json | 61 +++++ .../migration/003_init_default_roles.up.json | 24 ++ manager/migration/tool.go | 70 +++++ manager/repository/repo.go | 20 +- manager/rest/auth_hdl.go | 245 ++++++++++++++++- manager/rest/auth_hdl_test.go | 101 +++++++ manager/rest/hander.go | 79 +++++- manager/rest/handler_test.go | 76 +++++- manager/rest/middleware.go | 112 ++++++++ manager/rest/role_hdl.go | 217 +++++++++++++++ manager/rest/role_hdl_test.go | 113 ++++++++ manager/rest/routes.go | 35 +-- manager/service/auth_svc.go | 248 ++++++++++++++++-- manager/service/role_svc.go | 102 +++++++ manager/service/svc.go | 28 +- pkg/container/mongo_container.go | 19 ++ pkg/logger/logger.go | 1 + pkg/util/dbclean.go | 7 + pkg/util/encrypt.go | 7 + pkg/util/encrypt_test.go | 4 +- pkg/util/logger.go | 24 -- 36 files changed, 1882 insertions(+), 179 deletions(-) create mode 100644 manager/domain/enums.go create mode 100644 manager/errs/errors.go create mode 100644 manager/migration/001_init_collections.down.json create mode 100644 manager/migration/001_init_collections.up.json create mode 100644 manager/migration/002_init_default_permissions.up.json create mode 100644 manager/migration/003_init_default_roles.up.json create mode 100644 manager/migration/tool.go create mode 100644 manager/rest/auth_hdl_test.go create mode 100644 manager/rest/middleware.go create mode 100644 manager/rest/role_hdl.go create mode 100644 manager/rest/role_hdl_test.go create mode 100644 manager/service/role_svc.go create mode 100644 pkg/util/dbclean.go delete mode 100644 pkg/util/logger.go diff --git a/Makefile b/Makefile index c91102e..f854a67 100644 --- a/Makefile +++ b/Makefile @@ -88,4 +88,11 @@ local-infra-up: local-run-manager: @echo "Running Manager locally..." - go run main.go manager --config-dir $(CURDIR)/config/manager_config.toml --config-name manager_config \ No newline at end of file + go run main.go manager --config-dir $(CURDIR)/config/manager_config.toml --config-name manager_config + +local-run-manger-migration: + go install -tags 'mongodb' github.com/golang-migrate/migrate/v4/cmd/migrate@latest + migrate \ + -path $(CURDIR)/manager/migration \ + -database "mongodb://test:test@localhost:27017/manager?authSource=admin" \ + -verbose up \ No newline at end of file diff --git a/config/manager_config.default.toml b/config/manager_config.default.toml index b425453..aef2c72 100644 --- a/config/manager_config.default.toml +++ b/config/manager_config.default.toml @@ -11,7 +11,7 @@ host = "localhost" port = "27017" user = "test" password = "test" -database = "appdb" +database = "manager" ca_pem = """ -----BEGIN CERTIFICATE----- YOUR_CERTIFICATE_HERE diff --git a/config/manager_config.go b/config/manager_config.go index 9fa01e1..7874ee9 100644 --- a/config/manager_config.go +++ b/config/manager_config.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "path/filepath" "runtime" "strings" @@ -8,6 +9,16 @@ import ( "github.com/spf13/viper" ) +type SecretValue string + +func (s SecretValue) String() string { + return "****" +} + +func (s SecretValue) Value() string { + return string(s) +} + type ServerConfig struct { Host string `mapstructure:"host"` } @@ -27,21 +38,26 @@ type ManageConfig struct { } type MongoDBConfig struct { - Database string `mapstructure:"database"` - CAPem string `mapstructure:"ca_pem"` - User string `mapstructure:"user"` - Password string `mapstructure:"password"` - Port string `mapstructure:"port"` - Host string `mapstructure:"host"` + Database string `mapstructure:"database"` + CAPem SecretValue `mapstructure:"ca_pem"` + User string `mapstructure:"user"` + Password SecretValue `mapstructure:"password"` + Port string `mapstructure:"port"` + Host string `mapstructure:"host"` + Options string `mapstructure:"options"` +} + +func (mc MongoDBConfig) GetURI() string { + return fmt.Sprintf("mongodb://%s:%s@%s:%s/?%s", mc.User, mc.Password.Value(), mc.Host, mc.Port, mc.Options) } type KeyConfig struct { - RsaPrivateKeyPem string `mapstructure:"rsa_private_key_pem"` + RsaPrivateKeyPem SecretValue `mapstructure:"rsa_private_key_pem"` } type AccountConfig struct { - AdminEmail string `mapstructure:"admin_email"` - AdminPassword string `mapstructure:"admin_password"` + AdminEmail string `mapstructure:"admin_email"` + AdminPassword SecretValue `mapstructure:"admin_password"` } var ( diff --git a/config/manager_config.test.toml b/config/manager_config.test.toml index 2a6cdb7..210d170 100644 --- a/config/manager_config.test.toml +++ b/config/manager_config.test.toml @@ -12,7 +12,8 @@ host = "localhost" port = "27017" user = "test" password = "test" -database = "appdb" +database = "manager" +options = "authSource=admin&ssl=false" [key] rsa_private_key_pem = """ diff --git a/go.mod b/go.mod index 23ff4a8..d4946bc 100644 --- a/go.mod +++ b/go.mod @@ -8,12 +8,16 @@ tool github.com/vektra/mockery/v3 require ( github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/golang-migrate/migrate/v4 v4.19.1 github.com/labstack/echo/v4 v4.13.4 github.com/ory/dockertest/v3 v3.12.0 + github.com/pkg/errors v0.9.1 + github.com/rs/xid v1.5.0 github.com/rs/zerolog v1.33.0 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 + go.mongodb.org/mongo-driver v1.17.6 go.mongodb.org/mongo-driver/v2 v2.4.0 go.uber.org/fx v1.24.0 golang.org/x/crypto v0.45.0 @@ -29,7 +33,7 @@ require ( github.com/containerd/continuity v0.4.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/docker/cli v27.4.1+incompatible // indirect - github.com/docker/docker v27.1.1+incompatible // indirect + github.com/docker/docker v28.3.3+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/fatih/structs v1.1.0 // indirect @@ -59,11 +63,11 @@ require ( github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/sys/user v0.3.0 // indirect github.com/moby/term v0.5.0 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/runc v1.2.3 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect diff --git a/go.sum b/go.sum index 0cba427..8f01be7 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,10 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= @@ -22,20 +26,30 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4= +github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI= github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= -github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= +github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= @@ -45,6 +59,8 @@ 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/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA= +github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -104,6 +120,10 @@ github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -124,6 +144,7 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= @@ -178,8 +199,20 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS 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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= +go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.mongodb.org/mongo-driver/v2 v2.4.0 h1:Oq6BmUAAFTzMeh6AonuDlgZMuAuEiUxoAD1koK5MuFo= go.mongodb.org/mongo-driver/v2 v2.4.0/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= diff --git a/manager/app/module.go b/manager/app/module.go index a02ddbf..c8fb637 100644 --- a/manager/app/module.go +++ b/manager/app/module.go @@ -67,7 +67,7 @@ func TestRepoModule(cfg config.ManageConfig, containerBuilder *container.Contain } _, err = container.RunMongoContainer(containerBuilder, "api_test_mongo", container.MongoContainerConnection{ Username: cfg.MongoDB.User, - Password: cfg.MongoDB.Password, + Password: string(cfg.MongoDB.Password), Database: cfg.MongoDB.Database, Port: cfg.MongoDB.Port, Host: cfg.MongoDB.Host, diff --git a/manager/domain/account.go b/manager/domain/account.go index 1cb1f2d..ce435fd 100644 --- a/manager/domain/account.go +++ b/manager/domain/account.go @@ -8,36 +8,55 @@ const ( UserStatusActive UserStatus = 1 UserStatusInactive UserStatus = 2 UserStatusWaitChangePassword UserStatus = 3 - UserStatusBanned UserStatus = 4 ) type User struct { - BaseEntity - Email string `bson:"email,omitempty"` + BaseEntity `bson:",inline"` + UserName string `bson:"username,omitempty"` Password EncryptedPassword `bson:"password,omitempty"` Status UserStatus `bson:"status,omitempty"` - RoleIDs []bson.ObjectID `bson:"_id,omitempty"` - PermissionKeys []string `bson:"permission_keys,omitempty"` + Roles []string `bson:"roles,omitempty"` + PermissionKeys []string `bson:"permissionKeys,omitempty"` } type Role struct { - BaseEntity - Name string `bson:"name,omitempty"` - Description string `bson:"description,omitempty"` - Policies []Policy `bson:"policies,omitempty"` + BaseEntity `bson:",inline"` + Name string `bson:"name,omitempty"` + Description string `bson:"description,omitempty"` + Policies []RolePolicy `bson:"policies,omitempty"` } -type Policy struct { - PermissionKey string `bson:"permission_key,omitempty"` - Self bool `bson:"self,omitempty"` - K8SNamespace string `bson:"k8s_namespace,omitempty"` - PolicyNamespace string `bson:"policy_namespace,omitempty"` +type UpdateRoleOptions struct { + Name *string `bson:"name,omitempty"` + Description *string `bson:"description,omitempty"` + Policies *[]RolePolicy `bson:"policies,omitempty"` +} + +type RolePolicy struct { + PermissionKey PermissionKey `bson:"permissionKey,omitempty"` + Self bool `bson:"self,omitempty"` + K8SNamespace string `bson:"k8sNamespace,omitempty"` + PolicyNamespace string `bson:"policeNamespace,omitempty"` } type Permission struct { - BaseEntity - Key string `bson:"key,omitempty"` - Description string `bson:"description,omitempty"` - Resource string `bson:"resource,omitempty"` - Action string `bson:"action,omitempty"` + ID bson.ObjectID `bson:"_id,omitempty"` + Key PermissionKey `bson:"key,omitempty"` + Description string `bson:"description,omitempty"` + Resource string `bson:"resource,omitempty"` + Action PermissionAction `bson:"action,omitempty"` +} + +type UpdateUserPermissionsOptions struct { + Roles *[]string + Status *UserStatus } + +type PermissionAction string + +var ( + PermissionActionCreate PermissionAction = "create" + PermissionActionRead PermissionAction = "read" + PermissionActionUpdate PermissionAction = "update" + PermissionActionDelete PermissionAction = "delete" +) diff --git a/manager/domain/auth.go b/manager/domain/auth.go index 6161a1b..a15d61e 100644 --- a/manager/domain/auth.go +++ b/manager/domain/auth.go @@ -1,10 +1,17 @@ package domain -import "github.com/golang-jwt/jwt/v5" +import ( + "github.com/golang-jwt/jwt/v5" + "go.mongodb.org/mongo-driver/v2/bson" +) // Claims represents JWT token claims type Claims struct { - UID string `json:"uid"` - Roles []string `json:"roles"` + UID string `json:"uid"` + NeedChangePassword bool `json:"needChangePassword"` jwt.RegisteredClaims } + +func (c *Claims) GetBsonObjectUID() (bson.ObjectID, error) { + return bson.ObjectIDFromHex(c.UID) +} diff --git a/manager/domain/enums.go b/manager/domain/enums.go new file mode 100644 index 0000000..645b788 --- /dev/null +++ b/manager/domain/enums.go @@ -0,0 +1,19 @@ +package domain + +type PermissionKey string + +const ( + CreateUser PermissionKey = "user.create" + UserRead PermissionKey = "user.read" + ChangeUserPermission PermissionKey = "user.permission.update" + ResetUserPassword PermissionKey = "user.password.reset" + RoleCrete PermissionKey = "role.create" + RoleRead PermissionKey = "role.read" + RoleUpdate PermissionKey = "role.update" + RoleDelete PermissionKey = "role.delete" + PermissionRead PermissionKey = "permission.read" +) + +const ( + AdminRole = "admin" +) diff --git a/manager/domain/interface.go b/manager/domain/interface.go index 98120c2..980adaf 100644 --- a/manager/domain/interface.go +++ b/manager/domain/interface.go @@ -7,9 +7,9 @@ import ( ) type QueryUserOptions struct { - IDs []bson.ObjectID - Email string - Result []*User + IDs []bson.ObjectID + UserNames []string + Result []*User } type QueryRoleOptions struct { @@ -47,18 +47,20 @@ type Repository interface { } type Service interface { - SignUp(ctx context.Context, email, password string) error - Login(ctx context.Context, email, password string) (string, error) - Logout(ctx context.Context, token string) error - ChangePassword(ctx context.Context, email string, oldPassword, newPassword string) error - CreateUser(ctx context.Context, operator Claims, user *User) error - DeleteUser(ctx context.Context, operator Claims, userID bson.ObjectID) error - UpdateUser(ctx context.Context, operator Claims, user *User) error - ListAuditLogs(ctx context.Context, opt *QueryAuditLogOptions) error - CreateRole(ctx context.Context, role *Role) error - UpdateRole(ctx context.Context, role *Role) error + CreateNewUser(ctx context.Context, operator *Claims, username, password string) error + CreateAdminUserIfNotExists(ctx context.Context, username, password string) error + Login(ctx context.Context, email, password string) (token string, err error) + ChangePassword(ctx context.Context, user *Claims, oldPassword, newPassword string) error + ResetPassword(ctx context.Context, operator *Claims, id, newPassword string) error + UpdateUserPermissions(ctx context.Context, operator *Claims, id string, opt UpdateUserPermissionsOptions) error + VerifyJWTToken(ctx context.Context, tokenString string, permissionKey PermissionKey) (Claims, RolePolicy, error) + QueryUsers(ctx context.Context, opt *QueryUserOptions) error + + CreateRole(ctx context.Context, operator *Claims, role *Role) error + UpdateRole(ctx context.Context, operator *Claims, roleID string, opt UpdateRoleOptions) error + DeleteRole(ctx context.Context, operator *Claims, roleID string) error QueryRoles(ctx context.Context, opt *QueryRoleOptions) error - CreatePermission(ctx context.Context, permission *Permission) error - UpdatePermission(ctx context.Context, permission *Permission) error QueryPermissions(ctx context.Context, opt *QueryPermissionOptions) error + + ListAuditLogs(ctx context.Context, opt *QueryAuditLogOptions) error } diff --git a/manager/domain/value.go b/manager/domain/value.go index 0dce7ce..56ed053 100644 --- a/manager/domain/value.go +++ b/manager/domain/value.go @@ -1,21 +1,36 @@ package domain import ( + "fmt" "time" "github.com/Gthulhu/api/pkg/util" "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" ) type EncryptedPassword string func (value EncryptedPassword) MarshalBSONValue() (typ byte, data []byte, err error) { - pwdHash, err := util.CreateArgon2Hash(string(value)) - return byte(bson.TypeString), []byte(pwdHash), err + valStr := string(value) + if util.IsArgon2Hash(valStr) { + return byte(bson.TypeString), bsoncore.AppendString(nil, valStr), nil + } + pwdHash, err := util.CreateArgon2Hash(valStr) + return byte(bson.TypeString), bsoncore.AppendString(nil, string(pwdHash)), err } func (value *EncryptedPassword) UnmarshalBSONValue(typ byte, data []byte) error { - *value = EncryptedPassword(string(data)) + if typ != byte(bson.TypeString) { + return fmt.Errorf("invalid type %v for EncryptedPassword", bson.Type(typ)) + } + + str, _, ok := bsoncore.ReadString(data) + if !ok { + return fmt.Errorf("failed to read bson string") + } + + *value = EncryptedPassword(str) return nil } @@ -33,11 +48,11 @@ func (value EncryptedPassword) Cmp(plainText string) (bool, error) { type BaseEntity struct { ID bson.ObjectID `bson:"_id,omitempty"` - CreatedTime int64 `bson:"created_time,omitempty"` - UpdatedTime int64 `bson:"updated_time,omitempty"` - DeletedTime int64 `bson:"deleted_time,omitempty"` - CreatorID bson.ObjectID `bson:"creator_id,omitempty"` - UpdaterID bson.ObjectID `bson:"updater_id,omitempty"` + CreatedTime int64 `bson:"createdTime,omitempty"` + UpdatedTime int64 `bson:"updatedTime,omitempty"` + DeletedTime int64 `bson:"deletedTime,omitempty"` + CreatorID bson.ObjectID `bson:"creatorID,omitempty"` + UpdaterID bson.ObjectID `bson:"updaterID,omitempty"` } func NewBaseEntity(creatorID, updaterID *bson.ObjectID) BaseEntity { diff --git a/manager/errs/errors.go b/manager/errs/errors.go new file mode 100644 index 0000000..bcf77b7 --- /dev/null +++ b/manager/errs/errors.go @@ -0,0 +1,34 @@ +package errs + +import ( + "fmt" + + "github.com/pkg/errors" +) + +type HTTPStatusError struct { + StatusCode int + Message string + OriginalErr error +} + +func (e *HTTPStatusError) Error() string { + return fmt.Sprintf("(status %d) %s: %w", e.StatusCode, e.Message, e.OriginalErr) +} + +func NewHTTPStatusError(statusCode int, message string, originalErr error) *HTTPStatusError { + return &HTTPStatusError{ + StatusCode: statusCode, + Message: message, + OriginalErr: originalErr, + } +} + +func IsHTTPStatusError(err error) (*HTTPStatusError, bool) { + if err == nil { + return nil, false + } + err = errors.Cause(err) + httpErr, ok := err.(*HTTPStatusError) + return httpErr, ok +} diff --git a/manager/migration/001_init_collections.down.json b/manager/migration/001_init_collections.down.json new file mode 100644 index 0000000..03c7d0c --- /dev/null +++ b/manager/migration/001_init_collections.down.json @@ -0,0 +1,5 @@ +[ + { "drop": "users" }, + { "drop": "roles" }, + { "drop": "permissions" } +] diff --git a/manager/migration/001_init_collections.up.json b/manager/migration/001_init_collections.up.json new file mode 100644 index 0000000..d7d6b97 --- /dev/null +++ b/manager/migration/001_init_collections.up.json @@ -0,0 +1,182 @@ +[ + { + "create": "users", + "validator": { + "$jsonSchema": { + "bsonType": "object", + "required": [ + "username", + "password", + "status" + ], + "properties": { + "_id": { + "bsonType": "objectId" + }, + "username": { + "bsonType": "string" + }, + "password": { + "bsonType": "string" + }, + "status": { + "bsonType": "int" + }, + "roleIds": { + "bsonType": "array", + "items": { + "bsonType": "objectId" + } + }, + "permissionKeys": { + "bsonType": "array", + "items": { + "bsonType": "string" + } + }, + "createdTime": { + "bsonType": "long" + }, + "updatedTime": { + "bsonType": "long" + }, + "deletedTime": { + "bsonType": "long" + }, + "creatorID": { + "bsonType": "objectId" + }, + "updaterID": { + "bsonType": "objectId" + } + } + } + } + }, + { + "createIndexes": "users", + "indexes": [ + { + "key": { + "username": 1 + }, + "name": "idx_users_username_unique", + "unique": true + } + ] + }, + { + "create": "roles", + "validator": { + "$jsonSchema": { + "bsonType": "object", + "required": [ + "name" + ], + "properties": { + "_id": { + "bsonType": "objectId" + }, + "name": { + "bsonType": "string" + }, + "description": { + "bsonType": "string" + }, + "policies": { + "bsonType": "array", + "items": { + "bsonType": "object", + "required": [ + "permissionKey" + ], + "properties": { + "permissionKey": { + "bsonType": "string" + }, + "self": { + "bsonType": "bool" + }, + "k8sNamespace": { + "bsonType": "string" + }, + "policyNamespace": { + "bsonType": "string" + } + } + } + }, + "createdTime": { + "bsonType": "long" + }, + "updatedTime": { + "bsonType": "long" + }, + "deletedTime": { + "bsonType": "long" + }, + "creatorID": { + "bsonType": "objectId" + }, + "updaterID": { + "bsonType": "objectId" + } + } + } + } + }, + { + "createIndexes": "roles", + "indexes": [ + { + "key": { + "name": 1 + }, + "unique": true, + "name": "idx_roles_name_unique" + } + ] + }, + { + "create": "permissions", + "validator": { + "$jsonSchema": { + "bsonType": "object", + "required": [ + "key", + "resource", + "action" + ], + "properties": { + "_id": { + "bsonType": "objectId" + }, + "key": { + "bsonType": "string" + }, + "description": { + "bsonType": "string" + }, + "resource": { + "bsonType": "string" + }, + "action": { + "bsonType": "string" + } + } + } + } + }, + { + "createIndexes": "permissions", + "indexes": [ + { + "key": { + "key": 1 + }, + "unique": true, + "name": "idx_permissions_key_unique" + } + ] + } +] \ No newline at end of file diff --git a/manager/migration/002_init_default_permissions.up.json b/manager/migration/002_init_default_permissions.up.json new file mode 100644 index 0000000..a616560 --- /dev/null +++ b/manager/migration/002_init_default_permissions.up.json @@ -0,0 +1,61 @@ +[ + { + "insert": "permissions", + "documents": [ + { + "key": "user.create", + "resource": "user", + "action": "create", + "description": "Create a new user" + }, + { + "key": "user.password.reset", + "resource": "user", + "action": "update", + "description": "Reset user password" + }, + { + "key": "user.permission.update", + "resource": "user.permission", + "action": "update", + "description": "Update user permissions" + }, + { + "key": "user.read", + "resource": "user", + "action": "read", + "description": "Read user information" + }, + { + "key": "role.create", + "resource": "role", + "action": "create", + "description": "Create roles" + }, + { + "key": "role.delete", + "resource": "role", + "action": "delete", + "description": "Delete roles" + }, + { + "key": "role.update", + "resource": "role", + "action": "update", + "description": "Update roles" + }, + { + "key": "role.read", + "resource": "role", + "action": "read", + "description": "Read roles" + }, + { + "key": "permission.read", + "resource": "permission", + "action": "read", + "description": "Read permissions" + } + ] + } +] \ No newline at end of file diff --git a/manager/migration/003_init_default_roles.up.json b/manager/migration/003_init_default_roles.up.json new file mode 100644 index 0000000..41187c3 --- /dev/null +++ b/manager/migration/003_init_default_roles.up.json @@ -0,0 +1,24 @@ +[ + { + "insert": "roles", + "documents": [ + { + "name": "admin", + "description": "Administrator role with all permissions", + "policies": [ + { "permissionKey": "user.create", "self": false }, + { "permissionKey": "user.read", "self": false }, + { "permissionKey": "user.permission.update", "self": false }, + { "permissionKey": "user.password.reset", "self": false }, + { "permissionKey": "role.create", "self": false }, + { "permissionKey": "role.read", "self": false }, + { "permissionKey": "role.update", "self": false }, + { "permissionKey": "role.delete", "self": false }, + { "permissionKey": "permission.read", "self": false } + ], + "created_time": { "$numberLong": "0" }, + "updated_time": { "$numberLong": "0" } + } + ] + } +] diff --git a/manager/migration/tool.go b/manager/migration/tool.go new file mode 100644 index 0000000..14d255b --- /dev/null +++ b/manager/migration/tool.go @@ -0,0 +1,70 @@ +package migration + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "path/filepath" + "runtime" + "time" + + "github.com/Gthulhu/api/config" + "github.com/golang-migrate/migrate/v4" + mongodbmigrate "github.com/golang-migrate/migrate/v4/database/mongodb" + _ "github.com/golang-migrate/migrate/v4/source/file" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func RunMongoMigration(mongodbCfg config.MongoDBConfig) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + uri := mongodbCfg.GetURI() + dbName := mongodbCfg.Database + + mongoOpts := options.Client().ApplyURI(uri) + if mongodbCfg.CAPem != "" { + caPool := x509.NewCertPool() + caPool.AppendCertsFromPEM([]byte(mongodbCfg.CAPem)) + tlsConfig := &tls.Config{ + RootCAs: caPool, + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: false, + } + mongoOpts.SetTLSConfig(tlsConfig) + } + client, err := mongo.Connect(ctx, mongoOpts) + if err != nil { + return fmt.Errorf("connect to mongodb: %w, uri:%s", err, uri) + } + + driverConfig := &mongodbmigrate.Config{ + DatabaseName: dbName, + } + + driver, err := mongodbmigrate.WithInstance(client, driverConfig) + if err != nil { + return fmt.Errorf("mongodb driver error: %w", err) + } + + _, f, _, _ := runtime.Caller(0) + dir := filepath.Dir(f) + migrationsPath := dir + m, err := migrate.NewWithDatabaseInstance( + "file://"+migrationsPath, + dbName, + driver, + ) + if err != nil { + return fmt.Errorf("migrate init error: %w, uri:%s", err, uri) + } + + err = m.Up() + + if err != nil && err != migrate.ErrNoChange { + return fmt.Errorf("migrate up error: %w, uri:%s", err, uri) + } + + return nil +} diff --git a/manager/repository/repo.go b/manager/repository/repo.go index d5e848c..a937e1f 100644 --- a/manager/repository/repo.go +++ b/manager/repository/repo.go @@ -26,7 +26,7 @@ func NewRepository(params Params) (domain.Repository, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() - uri := fmt.Sprintf("mongodb://%s:%s@%s:%s", params.MongoConfig.User, params.MongoConfig.Password, params.MongoConfig.Host, params.MongoConfig.Port) + uri := params.MongoConfig.GetURI() mongoOpts := options.Client().ApplyURI(uri) if params.MongoConfig.CAPem != "" { @@ -40,7 +40,7 @@ func NewRepository(params Params) (domain.Repository, error) { mongoOpts.SetTLSConfig(tlsConfig) } - client, err := mongo.Connect() + client, err := mongo.Connect(mongoOpts) if err != nil { return nil, fmt.Errorf("connect to mongodb: %w, uri:%s", err, uri) } @@ -125,8 +125,8 @@ func (r *repo) QueryUsers(ctx context.Context, opt *domain.QueryUserOptions) err if len(opt.IDs) > 0 { filter["_id"] = bson.M{"$in": opt.IDs} } - if opt.Email != "" { - filter["email"] = opt.Email + if len(opt.UserNames) > 0 { + filter["username"] = bson.M{"$in": opt.UserNames} } cursor, err := r.db.Collection(userCollection).Find(ctx, filter) @@ -216,14 +216,14 @@ func (r *repo) CreatePermission(ctx context.Context, permission *domain.Permissi return errors.New("nil permission") } - now := time.Now().UnixMilli() + // now := time.Now().UnixMilli() if permission.ID.IsZero() { permission.ID = bson.NewObjectID() } - if permission.CreatedTime == 0 { - permission.CreatedTime = now - } - permission.UpdatedTime = now + // if permission.CreatedTime == 0 { + // permission.CreatedTime = now + // } + // permission.UpdatedTime = now res, err := r.db.Collection(permissionCollection).InsertOne(ctx, permission) if err != nil { @@ -243,7 +243,7 @@ func (r *repo) UpdatePermission(ctx context.Context, permission *domain.Permissi return errors.New("permission id is required") } - permission.UpdatedTime = time.Now().UnixMilli() + // permission.UpdatedTime = time.Now().UnixMilli() res, err := r.db.Collection(permissionCollection).ReplaceOne(ctx, bson.M{"_id": permission.ID}, permission) if err != nil { return fmt.Errorf("update permission, err: %w", err) diff --git a/manager/rest/auth_hdl.go b/manager/rest/auth_hdl.go index 3faff46..66a9b0c 100644 --- a/manager/rest/auth_hdl.go +++ b/manager/rest/auth_hdl.go @@ -1,23 +1,260 @@ package rest -import "net/http" +import ( + "errors" + "net/http" + + "github.com/Gthulhu/api/manager/domain" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type CreateUserRequest struct { + UserName string `json:"username"` + Password string `json:"password"` +} func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var req CreateUserRequest + err := h.JSONBind(r, &req) + if err != nil { + h.ErrorResponse(ctx, w, http.StatusBadRequest, "Invalid request body", err) + return + } + + claims, ok := h.GetClaimsFromContext(ctx) + if !ok { + h.ErrorResponse(ctx, w, http.StatusUnauthorized, "Unauthorized", errors.New("claims not found")) + return + } + err = h.Svc.CreateNewUser(ctx, &claims, req.UserName, req.Password) + if err != nil { + h.HandleError(ctx, w, err) + return + } + + response := NewSuccessResponse[string](nil) + h.JSONResponse(ctx, w, http.StatusOK, response) } -func (h *Handler) DeleteUser(w http.ResponseWriter, r *http.Request) { +type LoginRequest struct { + UserName string `json:"username"` + Password string `json:"password"` +} +type LoginResponse struct { + Token string `json:"token"` } -func (h *Handler) UpdateUser(w http.ResponseWriter, r *http.Request) { +func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var req LoginRequest + err := h.JSONBind(r, &req) + if err != nil { + h.ErrorResponse(ctx, w, http.StatusBadRequest, "Invalid request body", err) + return + } + if req.UserName == "" || req.Password == "" { + h.ErrorResponse(ctx, w, http.StatusUnprocessableEntity, "Username and password are required", errors.New("username or password is empty")) + return + } + token, err := h.Svc.Login(ctx, req.UserName, req.Password) + if err != nil { + h.HandleError(ctx, w, err) + return + } + respData := LoginResponse{ + Token: token, + } + response := NewSuccessResponse(&respData) + h.JSONResponse(ctx, w, http.StatusOK, response) +} + +type ChangePasswordRequest struct { + OldPassword string `json:"oldPassword"` + NewPassword string `json:"newPassword"` } func (h *Handler) ChangePassword(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var req ChangePasswordRequest + err := h.JSONBind(r, &req) + if err != nil { + h.ErrorResponse(ctx, w, http.StatusBadRequest, "Invalid request body", err) + return + } + + claims, ok := h.GetClaimsFromContext(ctx) + if !ok { + h.ErrorResponse(ctx, w, http.StatusUnauthorized, "Unauthorized", errors.New("claims not found")) + return + } + if claims.UID == "" { + h.ErrorResponse(ctx, w, http.StatusUnauthorized, "Unauthorized", errors.New("uid not found in claims")) + return + } + err = h.Svc.ChangePassword(ctx, &claims, req.OldPassword, req.NewPassword) + if err != nil { + h.HandleError(ctx, w, err) + return + } + + response := NewSuccessResponse[string](nil) + h.JSONResponse(ctx, w, http.StatusOK, response) } -func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { +type ResetPasswordRequest struct { + UserID string `json:"userID"` + NewPassword string `json:"newPassword"` +} + +func (h *Handler) ResetPassword(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var req ResetPasswordRequest + err := h.JSONBind(r, &req) + if err != nil { + h.ErrorResponse(ctx, w, http.StatusBadRequest, "Invalid request body", err) + return + } + + claims, ok := h.GetClaimsFromContext(ctx) + if !ok { + h.ErrorResponse(ctx, w, http.StatusUnauthorized, "Unauthorized", errors.New("claims not found")) + return + } + err = h.VerifyResourcePolicy(ctx, req.UserID) + if err != nil { + h.HandleError(ctx, w, err) + return + } + + err = h.Svc.ResetPassword(ctx, &claims, req.UserID, req.NewPassword) + if err != nil { + h.HandleError(ctx, w, err) + return + } + + response := NewSuccessResponse[string](nil) + h.JSONResponse(ctx, w, http.StatusOK, response) +} + +type UpdateUserPermissionsRequest struct { + UserID string `json:"userID"` + Roles *[]string `json:"roles,omitempty"` + Status *domain.UserStatus `json:"status,omitempty"` +} + +func (h *Handler) UpdateUserPermissions(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var req UpdateUserPermissionsRequest + err := h.JSONBind(r, &req) + if err != nil { + h.ErrorResponse(ctx, w, http.StatusBadRequest, "Invalid request body", err) + } + claims, ok := h.GetClaimsFromContext(ctx) + if !ok { + h.ErrorResponse(ctx, w, http.StatusUnauthorized, "Unauthorized", errors.New("claims not found")) + return + } + err = h.VerifyResourcePolicy(ctx, req.UserID) + if err != nil { + h.HandleError(ctx, w, err) + return + } + + err = h.Svc.UpdateUserPermissions(ctx, &claims, req.UserID, domain.UpdateUserPermissionsOptions{ + Roles: req.Roles, + Status: req.Status, + }) + if err != nil { + h.HandleError(ctx, w, err) + return + } + response := NewSuccessResponse[string](nil) + h.JSONResponse(ctx, w, http.StatusOK, response) +} + +type ListUsersResponse struct { + Users []struct { + ID string `json:"id"` + UserName string `json:"username"` + Roles []string `json:"roles"` + Status domain.UserStatus `json:"status"` + } `json:"users"` +} + +func (h *Handler) ListUsers(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + query := domain.QueryUserOptions{} + err := h.Svc.QueryUsers(ctx, &query) + if err != nil { + h.HandleError(ctx, w, err) + return + } + respData := ListUsersResponse{} + for _, user := range query.Result { + userInfo := struct { + ID string `json:"id"` + UserName string `json:"username"` + Roles []string `json:"roles"` + Status domain.UserStatus `json:"status"` + }{ + ID: user.ID.Hex(), + UserName: user.UserName, + Status: user.Status, + } + for _, role := range user.Roles { + userInfo.Roles = append(userInfo.Roles, role) + } + respData.Users = append(respData.Users, userInfo) + } + response := NewSuccessResponse(&respData) + h.JSONResponse(ctx, w, http.StatusOK, response) +} + +type GetSelfUserResponse struct { + ID string `json:"id"` + UserName string `json:"username"` + Roles []string `json:"roles"` + Status domain.UserStatus `json:"status"` +} +func (h *Handler) GetSelfUser(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + claims, ok := h.GetClaimsFromContext(ctx) + if !ok { + h.ErrorResponse(ctx, w, http.StatusUnauthorized, "Unauthorized", errors.New("claims not found")) + return + } + uid, err := claims.GetBsonObjectUID() + if err != nil { + h.ErrorResponse(ctx, w, http.StatusUnauthorized, "Unauthorized", errors.New("invalid user ID in claims")) + return + } + query := domain.QueryUserOptions{ + IDs: []bson.ObjectID{uid}, + } + err = h.Svc.QueryUsers(ctx, &query) + if err != nil { + h.HandleError(ctx, w, err) + return + } + if len(query.Result) == 0 { + h.ErrorResponse(ctx, w, http.StatusUnauthorized, "Unauthorized", errors.New("user not found")) + return + } + user := query.Result[0] + respData := GetSelfUserResponse{ + ID: user.ID.Hex(), + UserName: user.UserName, + Status: user.Status, + } + for _, role := range user.Roles { + respData.Roles = append(respData.Roles, role) + } + response := NewSuccessResponse(&respData) + h.JSONResponse(ctx, w, http.StatusOK, response) } diff --git a/manager/rest/auth_hdl_test.go b/manager/rest/auth_hdl_test.go new file mode 100644 index 0000000..ebed687 --- /dev/null +++ b/manager/rest/auth_hdl_test.go @@ -0,0 +1,101 @@ +package rest_test + +import ( + "net/http" + + "github.com/Gthulhu/api/config" + "github.com/Gthulhu/api/manager/domain" + "github.com/Gthulhu/api/manager/rest" + "github.com/Gthulhu/api/pkg/util" +) + +func (suite *HandlerTestSuite) TestIntegrationAuthHandler() { + adminUser, adminPwd := config.GetConfig().Account.AdminEmail, config.GetConfig().Account.AdminPassword + adminToken := suite.login(adminUser, adminPwd.Value(), http.StatusOK) + + // Test creating a new user + newUsername := "testuser" + newPassword := "testpassword" + suite.createUser("", newUsername, newPassword, http.StatusUnauthorized) // without token should fail + suite.createUser(adminToken, newUsername, newPassword, http.StatusOK) // with admin token should succeed + users := suite.listUsers(adminToken, http.StatusOK, 2) // should have 2 users now + uid := "" + for _, u := range users.Users { + if u.UserName == newUsername { + uid = u.ID + break + } + } + suite.updateUserPermissions(adminToken, rest.UpdateUserPermissionsRequest{ + UserID: uid, + Roles: util.Ptr([]string{domain.AdminRole}), + }, http.StatusOK) // updating non-existing user should return not found + + // Get the user ID of the newly created user + // Test login with the new user + userToken := suite.login(newUsername, newPassword, http.StatusOK) + suite.listUsers(userToken, http.StatusForbidden, 0) // password hasn't been changed yet, should be forbidden + // Change password + newUserPassword := "newtestpassword" + suite.changePassword(userToken, newPassword, newUserPassword, http.StatusOK) + // Login with old password should fail + suite.login(newUsername, newPassword, http.StatusUnauthorized) + // Login with new password should succeed + userToken = suite.login(newUsername, newUserPassword, http.StatusOK) + // Now listing users should succeed + suite.listUsers(userToken, http.StatusOK, 2) +} + +func (suite *HandlerTestSuite) login(username, password string, expectedStatus int) string { + loginReq := rest.LoginRequest{ + UserName: username, + Password: password, + } + loginResp := rest.SuccessResponse[rest.LoginResponse]{} + + _, resp := suite.sendV1Request("POST", "/auth/login", loginReq, &loginResp, "") + suite.Require().Equal(expectedStatus, resp.Code, "Unexpected status code on login") + if expectedStatus == http.StatusOK { + suite.NotEmpty(loginResp.Data.Token, "Token should not be empty on successful login") + return loginResp.Data.Token + } + return "" +} + +func (suite *HandlerTestSuite) createUser(token, username, password string, expectedStatus int) { + createUserReq := rest.CreateUserRequest{ + UserName: username, + Password: password, + } + createUserResp := rest.SuccessResponse[string]{} + + _, resp := suite.sendV1Request("POST", "/users", createUserReq, &createUserResp, token) + suite.Require().Equal(expectedStatus, resp.Code, "Unexpected status code on create user") +} + +func (suite *HandlerTestSuite) listUsers(token string, expectedStatus int, expectedUserCnt int) rest.ListUsersResponse { + listUserResp := rest.SuccessResponse[rest.ListUsersResponse]{} + _, resp := suite.sendV1Request("GET", "/users", nil, &listUserResp, token) + suite.Equal(expectedStatus, resp.Code, "Unexpected status code on list users") + if expectedStatus == http.StatusOK { + suite.Require().Equal(expectedUserCnt, len(listUserResp.Data.Users), "Unexpected number of users returned") + return *listUserResp.Data + } + return rest.ListUsersResponse{} +} + +func (suite *HandlerTestSuite) updateUserPermissions(token string, req rest.UpdateUserPermissionsRequest, expectedStatus int) { + updatePermResp := rest.SuccessResponse[string]{} + _, resp := suite.sendV1Request("PUT", "/users/permissions", req, &updatePermResp, token) + suite.Require().Equal(expectedStatus, resp.Code, "Unexpected status code on update user permissions") +} + +func (suite *HandlerTestSuite) changePassword(token, oldPassword, newPassword string, expectedStatus int) { + changePwdReq := rest.ChangePasswordRequest{ + OldPassword: oldPassword, + NewPassword: newPassword, + } + changePwdResp := rest.SuccessResponse[string]{} + _, resp := suite.sendV1Request("PUT", "/users/self/password", changePwdReq, &changePwdResp, token) + suite.Require().Equal(expectedStatus, resp.Code, "Unexpected status code on change password") +} diff --git a/manager/rest/hander.go b/manager/rest/hander.go index aeb51c3..e26d105 100644 --- a/manager/rest/hander.go +++ b/manager/rest/hander.go @@ -3,10 +3,12 @@ package rest import ( "context" "encoding/json" + "errors" "net/http" "time" "github.com/Gthulhu/api/manager/domain" + "github.com/Gthulhu/api/manager/errs" "github.com/Gthulhu/api/pkg/logger" "go.uber.org/fx" ) @@ -17,10 +19,18 @@ type ErrorResponse struct { Error string `json:"error"` } +func NewSuccessResponse[T any](data *T) SuccessResponse[T] { + return SuccessResponse[T]{ + Success: true, + Data: data, + Timestamp: time.Now().UTC().Format(time.RFC3339), + } +} + // SuccessResponse represents the success response structure -type SuccessResponse struct { +type SuccessResponse[T any] struct { Success bool `json:"success"` - Message string `json:"message"` + Data *T `json:"data,omitempty"` Timestamp string `json:"timestamp"` } @@ -58,7 +68,23 @@ func (h *Handler) JSONBind(r *http.Request, dst any) error { return nil } -func (h *Handler) ErrorResponse(ctx context.Context, w http.ResponseWriter, status int, errMsg string) { +func (h *Handler) HandleError(ctx context.Context, w http.ResponseWriter, err error) { + httpErr, ok := errs.IsHTTPStatusError(err) + if ok { + h.ErrorResponse(ctx, w, httpErr.StatusCode, httpErr.Message, httpErr.OriginalErr) + return + } + h.ErrorResponse(ctx, w, http.StatusInternalServerError, "Internal Server Error", err) +} + +func (h *Handler) ErrorResponse(ctx context.Context, w http.ResponseWriter, status int, errMsg string, err error) { + if err != nil { + if status >= 500 { + logger.Logger(ctx).Error().Err(err).Msg(errMsg) + } else { + logger.Logger(ctx).Warn().Err(err).Msg(errMsg) + } + } resp := ErrorResponse{ Success: false, Error: errMsg, @@ -66,15 +92,6 @@ func (h *Handler) ErrorResponse(ctx context.Context, w http.ResponseWriter, stat h.JSONResponse(ctx, w, status, resp) } -func (h *Handler) SuccessResponse(ctx context.Context, w http.ResponseWriter, message string) { - resp := SuccessResponse{ - Success: true, - Message: message, - Timestamp: time.Now().UTC().Format(time.RFC3339), - } - h.JSONResponse(ctx, w, http.StatusOK, resp) -} - func (h *Handler) Version(w http.ResponseWriter, r *http.Request) { response := map[string]string{ "message": "BSS Metrics API Server", @@ -92,3 +109,41 @@ func (h *Handler) HealthCheck(w http.ResponseWriter, r *http.Request) { } h.JSONResponse(r.Context(), w, http.StatusOK, response) } + +type claimsKey struct{} + +// GetClaimsFromContext extracts domain.Claims from the request context +func (h *Handler) GetClaimsFromContext(ctx context.Context) (domain.Claims, bool) { + claims, ok := ctx.Value(claimsKey{}).(domain.Claims) + return claims, ok +} + +func (h *Handler) SetClaimsInContext(ctx context.Context, claims domain.Claims) context.Context { + return context.WithValue(ctx, claimsKey{}, claims) +} + +type rolePolicyKey struct{} + +func (h *Handler) SetRolePolicyInContext(ctx context.Context, rolePolicy domain.RolePolicy) context.Context { + return context.WithValue(ctx, rolePolicyKey{}, rolePolicy) +} + +func (h *Handler) GetRolePolicyFromContext(ctx context.Context) (domain.RolePolicy, bool) { + rolePolicy, ok := ctx.Value(rolePolicyKey{}).(domain.RolePolicy) + return rolePolicy, ok +} + +func (h *Handler) VerifyResourcePolicy(ctx context.Context, resourceOwnerID string) error { + claims, ok := h.GetClaimsFromContext(ctx) + if !ok { + return errs.NewHTTPStatusError(http.StatusUnauthorized, "unauthorized", errors.New("claims not found in context")) + } + rolePolicy, ok := h.GetRolePolicyFromContext(ctx) + if !ok { + return errs.NewHTTPStatusError(http.StatusUnauthorized, "unauthorized", errors.New("role policy not found in context")) + } + if rolePolicy.Self && claims.UID != resourceOwnerID { + return errs.NewHTTPStatusError(http.StatusForbidden, "forbidden", errors.New("access to resource denied")) + } + return nil +} diff --git a/manager/rest/handler_test.go b/manager/rest/handler_test.go index 8307377..e2282fc 100644 --- a/manager/rest/handler_test.go +++ b/manager/rest/handler_test.go @@ -1,18 +1,27 @@ package rest_test import ( + "bytes" "context" + "crypto/tls" + "crypto/x509" "encoding/json" + "io" "net/http" "net/http/httptest" "testing" "github.com/Gthulhu/api/config" "github.com/Gthulhu/api/manager/app" + "github.com/Gthulhu/api/manager/migration" "github.com/Gthulhu/api/manager/rest" "github.com/Gthulhu/api/pkg/container" + "github.com/Gthulhu/api/pkg/logger" + "github.com/Gthulhu/api/pkg/util" "github.com/labstack/echo/v4" "github.com/stretchr/testify/suite" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" "go.uber.org/fx" ) @@ -26,9 +35,12 @@ type HandlerTestSuite struct { Ctx context.Context Engine *echo.Echo *container.ContainerBuilder + mongoDBClient *mongo.Client + mongoDBCfg config.MongoDBConfig } func (suite *HandlerTestSuite) SetupSuite() { + logger.InitLogger() suite.Ctx = context.Background() containerBuilder, err := container.NewContainerBuilder("") suite.Require().NoError(err, "Failed to create container builder") @@ -47,7 +59,12 @@ func (suite *HandlerTestSuite) SetupSuite() { suite.Require().NoError(err, "Failed to create handler module") opt := fx.Options( handlerModule, + fx.Invoke(migration.RunMongoMigration), fx.Populate(&suite.Handler), + fx.Invoke(func(mongoDBCfg config.MongoDBConfig) { + suite.mongoDBCfg = mongoDBCfg + suite.newMongoClient() + }), ) err = fx.New(opt).Start(suite.Ctx) @@ -60,15 +77,25 @@ func (suite *HandlerTestSuite) SetupSuite() { suite.Handler.SetupRoutes(e) } +func (suite *HandlerTestSuite) SetupTest() { + err := util.MongoCleanup(suite.mongoDBClient, suite.mongoDBCfg.Database) + suite.Require().NoError(err, "Failed to clean up MongoDB") + err = migration.RunMongoMigration(suite.mongoDBCfg) + suite.Require().NoError(err, "Failed to run MongoDB migrations") + err = suite.Handler.Svc.CreateAdminUserIfNotExists(suite.Ctx, config.GetConfig().Account.AdminEmail, config.GetConfig().Account.AdminPassword.Value()) + suite.Require().NoError(err, "Failed to create admin user") +} + func (suite *HandlerTestSuite) TearDownSuite() { err := suite.ContainerBuilder.PruneAll() suite.Require().NoError(err, "Failed to terminate containers") } func (suite *HandlerTestSuite) JSONDecode(r *httptest.ResponseRecorder, dst any) { - decoder := json.NewDecoder(r.Body) - err := decoder.Decode(dst) - suite.Require().NoError(err, "Failed to decode JSON response") + rBody, err := io.ReadAll(r.Body) + suite.Require().NoError(err, "Failed to read response body") + err = json.Unmarshal(rBody, dst) + suite.Require().NoErrorf(err, "Failed to decode JSON response, body: %s", string(rBody)) } func (suite *HandlerTestSuite) TestHealthCheck() { @@ -81,3 +108,46 @@ func (suite *HandlerTestSuite) TestHealthCheck() { suite.JSONDecode(rec, &resp) suite.Equal("healthy", resp["status"].(string), "Expected status to be healthy") } + +func (suite *HandlerTestSuite) sendV1Request(method, path string, reqStruct any, respStruct any, token string) (*http.Request, *httptest.ResponseRecorder) { + reqBody := []byte{} + if reqStruct != nil { + var err error + reqBody, err = json.Marshal(reqStruct) + suite.Require().NoError(err, "Failed to marshal request body") + } + v1Path := "/api/v1" + path + req := httptest.NewRequest(method, v1Path, bytes.NewReader(reqBody)) + req.Header.Set("Content-Type", "application/json") + if token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } + rec := httptest.NewRecorder() + suite.Engine.ServeHTTP(rec, req) + if respStruct != nil { + suite.JSONDecode(rec, respStruct) + } + return req, rec +} + +func (suite *HandlerTestSuite) newMongoClient() { + uri := suite.mongoDBCfg.GetURI() + mongoOpts := options.Client().ApplyURI(uri) + if suite.mongoDBCfg.CAPem != "" { + caPool := x509.NewCertPool() + caPool.AppendCertsFromPEM([]byte(suite.mongoDBCfg.CAPem)) + tlsConfig := &tls.Config{ + RootCAs: caPool, + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: false, + } + mongoOpts.SetTLSConfig(tlsConfig) + } + + client, err := mongo.Connect(mongoOpts, nil) + suite.Require().NoError(err, "Failed to connect to MongoDB") + + err = client.Ping(suite.Ctx, nil) + suite.Require().NoError(err, "Failed to ping MongoDB") + suite.mongoDBClient = client +} diff --git a/manager/rest/middleware.go b/manager/rest/middleware.go new file mode 100644 index 0000000..46df273 --- /dev/null +++ b/manager/rest/middleware.go @@ -0,0 +1,112 @@ +package rest + +import ( + "bytes" + "net/http" + "runtime/debug" + "time" + + "github.com/Gthulhu/api/manager/domain" + "github.com/Gthulhu/api/pkg/logger" + "github.com/rs/xid" +) + +func (h *Handler) GetAuthMiddleware(permissionKey domain.PermissionKey) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + tokenString := r.Header.Get("Authorization") + if tokenString == "" { + h.ErrorResponse(ctx, w, http.StatusUnauthorized, "Missing Authorization header", nil) + return + } + + // parse bearer token + const bearerPrefix = "Bearer " + if len(tokenString) <= len(bearerPrefix) || tokenString[:len(bearerPrefix)] != bearerPrefix { + h.ErrorResponse(ctx, w, http.StatusUnauthorized, "Invalid Authorization header format", nil) + return + } + tokenString = tokenString[len(bearerPrefix):] + + claims, rolePolicy, err := h.Svc.VerifyJWTToken(ctx, tokenString, permissionKey) + if err != nil { + h.HandleError(ctx, w, err) + return + } + + ctx = h.SetClaimsInContext(ctx, claims) + ctx = h.SetRolePolicyInContext(ctx, rolePolicy) + r = r.WithContext(ctx) + next.ServeHTTP(w, r) + }) + } +} + +func LoggerMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + reqID := r.Header.Get("X-Request-ID") + if reqID == "" { + reqID = xid.New().String() + } + start := time.Now() + log := logger.Logger(ctx).With(). + Str("method", r.Method).Str("req_id", reqID). + Str("url", r.URL.String()).Logger() + + defer func() { + if err := recover(); err != nil { + log.Error().Interface("panic", err).Msgf("Recovered from panic, stack trace: %s", string(debug.Stack())) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } + }() + + ctx = log.WithContext(ctx) + r = r.WithContext(ctx) + responseWriter := NewResponseWriter(w) + next.ServeHTTP(responseWriter, r) + cost := time.Since(start) + log = log.With(). + Int("cost_msec", int(cost.Milliseconds())). + Logger() + if responseWriter.statusCode >= 500 { + log.Error(). + Int("status_code", responseWriter.statusCode). + Str("response_body", responseWriter.responseBody.String()). + Msg("Request completed with server error") + } else if responseWriter.statusCode >= 400 { + log.Warn(). + Int("status_code", responseWriter.statusCode). + Str("response_body", responseWriter.responseBody.String()). + Msg("Request completed with client error") + } else { + log.Info(). + Int("status_code", responseWriter.statusCode). + Msg("Request completed successfully") + } + }) +} + +type responseWriter struct { + http.ResponseWriter + responseBody bytes.Buffer + statusCode int +} + +func NewResponseWriter(w http.ResponseWriter) *responseWriter { + return &responseWriter{ + ResponseWriter: w, + } +} + +func (rw *responseWriter) WriteHeader(code int) { + rw.statusCode = code + rw.ResponseWriter.WriteHeader(code) +} + +func (rw *responseWriter) Write(b []byte) (int, error) { + rw.responseBody.Write(b) + return rw.ResponseWriter.Write(b) +} diff --git a/manager/rest/role_hdl.go b/manager/rest/role_hdl.go new file mode 100644 index 0000000..61263b9 --- /dev/null +++ b/manager/rest/role_hdl.go @@ -0,0 +1,217 @@ +package rest + +import ( + "errors" + "net/http" + + "github.com/Gthulhu/api/manager/domain" +) + +type RolePolicy struct { + PermissionKey domain.PermissionKey `json:"permissionKey"` + Self bool `json:"self"` + K8SNamespace string `json:"k8sNamespace"` + PolicyNamespace string `json:"policyNamespace"` +} + +type CreateRoleRequest struct { + Name string `json:"name"` + Description string `json:"description"` + RolePolicies []RolePolicy `json:"rolePolicies"` +} + +func (h *Handler) CreateRole(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var req CreateRoleRequest + err := h.JSONBind(r, &req) + if err != nil { + h.ErrorResponse(ctx, w, http.StatusBadRequest, "Invalid request body", err) + return + } + + claims, ok := h.GetClaimsFromContext(ctx) + if !ok { + h.ErrorResponse(ctx, w, http.StatusUnauthorized, "Unauthorized", errors.New("claims not found")) + return + } + role := domain.Role{ + Name: req.Name, + Description: req.Description, + } + for _, rp := range req.RolePolicies { + role.Policies = append(role.Policies, domain.RolePolicy{ + PermissionKey: rp.PermissionKey, + Self: rp.Self, + K8SNamespace: rp.K8SNamespace, + PolicyNamespace: rp.PolicyNamespace, + }) + } + + err = h.Svc.CreateRole(ctx, &claims, &role) + if err != nil { + h.HandleError(ctx, w, err) + return + } + + response := NewSuccessResponse[string](nil) + h.JSONResponse(ctx, w, http.StatusOK, response) +} + +type UpdateRoleRequest struct { + ID string `json:"id"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + RolePolicy *[]RolePolicy `json:"rolePolicy,omitempty"` +} + +func (h *Handler) UpdateRole(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var req UpdateRoleRequest + err := h.JSONBind(r, &req) + if err != nil { + h.ErrorResponse(ctx, w, http.StatusBadRequest, "Invalid request body", err) + return + } + + claims, ok := h.GetClaimsFromContext(ctx) + if !ok { + h.ErrorResponse(ctx, w, http.StatusUnauthorized, "Unauthorized", errors.New("claims not found")) + return + } + + updateOpts := domain.UpdateRoleOptions{} + if req.Name != nil { + updateOpts.Name = req.Name + } + if req.Description != nil { + updateOpts.Description = req.Description + } + if req.RolePolicy != nil { + var policies []domain.RolePolicy + for _, rp := range *req.RolePolicy { + policies = append(policies, domain.RolePolicy{ + PermissionKey: rp.PermissionKey, + Self: rp.Self, + K8SNamespace: rp.K8SNamespace, + PolicyNamespace: rp.PolicyNamespace, + }) + } + updateOpts.Policies = &policies + } + + err = h.Svc.UpdateRole(ctx, &claims, req.ID, updateOpts) + if err != nil { + h.HandleError(ctx, w, err) + return + } + + response := NewSuccessResponse[string](nil) + h.JSONResponse(ctx, w, http.StatusOK, response) +} + +type DeleteRoleRequest struct { + ID string `json:"id"` +} + +func (h *Handler) DeleteRole(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var req DeleteRoleRequest + err := h.JSONBind(r, &req) + if err != nil { + h.ErrorResponse(ctx, w, http.StatusBadRequest, "Invalid request body", err) + return + } + + claims, ok := h.GetClaimsFromContext(ctx) + if !ok { + h.ErrorResponse(ctx, w, http.StatusUnauthorized, "Unauthorized", errors.New("claims not found")) + return + } + + err = h.Svc.DeleteRole(ctx, &claims, req.ID) + if err != nil { + h.HandleError(ctx, w, err) + return + } + + response := NewSuccessResponse[string](nil) + h.JSONResponse(ctx, w, http.StatusOK, response) +} + +type ListRolesResponse struct { + Roles []struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + RolePolicy []RolePolicy `json:"rolePolicy"` + } `json:"roles"` +} + +func (h *Handler) ListRoles(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + queryOpts := &domain.QueryRoleOptions{} + err := h.Svc.QueryRoles(ctx, queryOpts) + if err != nil { + h.HandleError(ctx, w, err) + return + } + + var resp ListRolesResponse + for _, role := range queryOpts.Result { + r := struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + RolePolicy []RolePolicy `json:"rolePolicy"` + }{ + ID: role.ID.Hex(), + Name: role.Name, + Description: role.Description, + } + for _, rp := range role.Policies { + r.RolePolicy = append(r.RolePolicy, RolePolicy{ + PermissionKey: rp.PermissionKey, + Self: rp.Self, + K8SNamespace: rp.K8SNamespace, + PolicyNamespace: rp.PolicyNamespace, + }) + } + resp.Roles = append(resp.Roles, r) + } + + response := NewSuccessResponse[ListRolesResponse](&resp) + h.JSONResponse(ctx, w, http.StatusOK, response) + +} + +type ListPermissionsResponse struct { + Permissions []struct { + Key domain.PermissionKey `json:"key"` + Description string `json:"description"` + } `json:"permissions"` +} + +func (h *Handler) ListPermissions(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + queryOpts := &domain.QueryPermissionOptions{} + err := h.Svc.QueryPermissions(ctx, queryOpts) + if err != nil { + h.HandleError(ctx, w, err) + return + } + + var resp ListPermissionsResponse + for _, perm := range queryOpts.Result { + p := struct { + Key domain.PermissionKey `json:"key"` + Description string `json:"description"` + }{ + Key: perm.Key, + Description: perm.Description, + } + resp.Permissions = append(resp.Permissions, p) + } + + response := NewSuccessResponse[ListPermissionsResponse](&resp) + h.JSONResponse(ctx, w, http.StatusOK, response) +} diff --git a/manager/rest/role_hdl_test.go b/manager/rest/role_hdl_test.go new file mode 100644 index 0000000..81e25a4 --- /dev/null +++ b/manager/rest/role_hdl_test.go @@ -0,0 +1,113 @@ +package rest_test + +import ( + "net/http" + + "github.com/Gthulhu/api/config" + "github.com/Gthulhu/api/manager/domain" + "github.com/Gthulhu/api/manager/rest" + "github.com/Gthulhu/api/pkg/util" +) + +func (suite *HandlerTestSuite) TestIntegrationRoleHandler() { + adminUser, adminPwd := config.GetConfig().Account.AdminEmail, config.GetConfig().Account.AdminPassword + adminToken := suite.login(adminUser, adminPwd.Value(), http.StatusOK) + + roleManager := "role_manager" + rolePolicies := []rest.RolePolicy{ + { + PermissionKey: domain.RoleRead, + Self: false, + K8SNamespace: "", + PolicyNamespace: "", + }, + } + suite.createRole("", roleManager, rolePolicies, http.StatusUnauthorized) + suite.createRole(adminToken, roleManager, rolePolicies, http.StatusOK) + suite.listRoles(adminToken, http.StatusOK, 2) + + newUserName, newUserPwd := "rolemanager", "rolemanagerpwd" + suite.createUser(adminToken, newUserName, newUserPwd, http.StatusOK) + users := suite.listUsers(adminToken, http.StatusOK, 2) + newUserUID := "" + for _, r := range users.Users { + if r.UserName == newUserName { + newUserUID = r.ID + break + } + } + suite.Require().NotEmpty(newUserUID, "Newly created role not found in list roles response") + + changePwdToken := suite.login(newUserName, newUserPwd, http.StatusOK) + newUserNewPwd := "newrolemanagerpwd" + suite.changePassword(changePwdToken, newUserPwd, newUserNewPwd, http.StatusOK) + userToken := suite.login(newUserName, newUserNewPwd, http.StatusOK) + suite.listRoles(userToken, http.StatusForbidden, 0) + + suite.updateUserPermissions(adminToken, rest.UpdateUserPermissionsRequest{UserID: newUserUID, Roles: util.Ptr([]string{roleManager})}, http.StatusOK) + roles := suite.listRoles(userToken, http.StatusOK, 2) + roleID := "" + for _, r := range roles.Roles { + if r.Name == roleManager { + roleID = r.ID + break + } + } + suite.Require().NotEmpty(roleID, "Role not found in list roles response") + newPolicy := []rest.RolePolicy{ + { + PermissionKey: domain.RoleUpdate, + Self: false, + K8SNamespace: "", + PolicyNamespace: "", + }, + } + suite.updateRole(userToken, roleID, newPolicy, http.StatusForbidden) + suite.updateRole(adminToken, roleID, newPolicy, http.StatusOK) + + suite.listPermissions(userToken, http.StatusForbidden) + suite.updateRole(userToken, roleID, []rest.RolePolicy{{PermissionKey: domain.PermissionRead}}, http.StatusOK) + permissions := suite.listPermissions(userToken, http.StatusOK) + suite.Require().Greater(len(permissions.Permissions), 0, "Expected non-zero permissions") +} + +func (suite *HandlerTestSuite) createRole(token, roleName string, policies []rest.RolePolicy, expectedStatus int) { + createRoleReq := rest.CreateRoleRequest{ + Name: roleName, + RolePolicies: policies, + } + createRoleResp := rest.SuccessResponse[string]{} + _, resp := suite.sendV1Request("POST", "/roles", createRoleReq, &createRoleResp, token) + suite.Require().Equal(expectedStatus, resp.Code, "Unexpected status code on create role") +} + +func (suite *HandlerTestSuite) listRoles(token string, expectedStatus int, expectedRoleCnt int) rest.ListRolesResponse { + listRolesResp := rest.SuccessResponse[rest.ListRolesResponse]{} + _, resp := suite.sendV1Request("GET", "/roles", nil, &listRolesResp, token) + suite.Require().Equal(expectedStatus, resp.Code, "Unexpected status code on list roles") + if expectedStatus == http.StatusOK { + suite.Equal(expectedRoleCnt, len(listRolesResp.Data.Roles), "Unexpected number of roles returned") + return *listRolesResp.Data + } + return rest.ListRolesResponse{} +} + +func (suite *HandlerTestSuite) updateRole(token, roleID string, rolePolicies []rest.RolePolicy, expectedStatus int) { + updateRoleReq := rest.UpdateRoleRequest{ + ID: roleID, + RolePolicy: util.Ptr(rolePolicies), + } + updateRoleResp := rest.SuccessResponse[struct{}]{} + _, resp := suite.sendV1Request("PUT", "/roles", updateRoleReq, &updateRoleResp, token) + suite.Require().Equal(expectedStatus, resp.Code, "Unexpected status code on update role") +} + +func (suite *HandlerTestSuite) listPermissions(token string, expectedStatus int) rest.ListPermissionsResponse { + listPermissionsResp := rest.SuccessResponse[rest.ListPermissionsResponse]{} + _, resp := suite.sendV1Request("GET", "/permissions", nil, &listPermissionsResp, token) + suite.Require().Equal(expectedStatus, resp.Code, "Unexpected status code on list permissions") + if expectedStatus == http.StatusOK { + return *listPermissionsResp.Data + } + return rest.ListPermissionsResponse{} +} diff --git a/manager/rest/routes.go b/manager/rest/routes.go index ac7aa5f..4c49699 100644 --- a/manager/rest/routes.go +++ b/manager/rest/routes.go @@ -3,6 +3,7 @@ package rest import ( "net/http" + "github.com/Gthulhu/api/manager/domain" "github.com/labstack/echo/v4" ) @@ -10,14 +11,27 @@ func (h *Handler) SetupRoutes(engine *echo.Echo) { engine.GET("/health", h.echoHandler(h.HealthCheck)) engine.GET("/version", h.echoHandler(h.Version)) + api := engine.Group("/api", echo.WrapMiddleware(LoggerMiddleware)) // v1 routes { - // users & auth routes - engine.POST("/api/v1/users", h.echoHandler(h.CreateUser), echo.WrapMiddleware(h.AuthMiddleware)) - engine.DELETE("/api/v1/users", h.echoHandler(h.DeleteUser), echo.WrapMiddleware(h.AuthMiddleware)) - engine.PUT("/api/v1/users", h.echoHandler(h.UpdateUser), echo.WrapMiddleware(h.AuthMiddleware)) - engine.PUT("/api/v1/users/password", h.echoHandler(h.ChangePassword), echo.WrapMiddleware(h.AuthMiddleware)) - engine.POST("/api/v1/auth/login", h.echoHandler(h.Login), echo.WrapMiddleware(h.AuthMiddleware)) + apiV1 := api.Group("/v1") + // auth routes + apiV1.POST("/auth/login", h.echoHandler(h.Login)) + + // users routes + apiV1.POST("/users", h.echoHandler(h.CreateUser), echo.WrapMiddleware(h.GetAuthMiddleware(domain.CreateUser))) + apiV1.PUT("/users/password", h.echoHandler(h.ResetPassword), echo.WrapMiddleware(h.GetAuthMiddleware(domain.ResetUserPassword))) + apiV1.PUT("/users/permissions", h.echoHandler(h.UpdateUserPermissions), echo.WrapMiddleware(h.GetAuthMiddleware(domain.ChangeUserPermission))) + apiV1.GET("/users", h.echoHandler(h.ListUsers), echo.WrapMiddleware(h.GetAuthMiddleware(domain.UserRead))) + apiV1.PUT("/users/self/password", h.echoHandler(h.ChangePassword), echo.WrapMiddleware(h.GetAuthMiddleware(""))) + apiV1.GET("/users/self", h.echoHandler(h.GetSelfUser), echo.WrapMiddleware(h.GetAuthMiddleware(""))) + + // role routes + apiV1.POST("/roles", h.echoHandler(h.CreateRole), echo.WrapMiddleware(h.GetAuthMiddleware(domain.RoleCrete))) + apiV1.PUT("/roles", h.echoHandler(h.UpdateRole), echo.WrapMiddleware(h.GetAuthMiddleware(domain.RoleUpdate))) + apiV1.DELETE("/roles", h.echoHandler(h.DeleteRole), echo.WrapMiddleware(h.GetAuthMiddleware(domain.RoleDelete))) + apiV1.GET("/roles", h.echoHandler(h.ListRoles), echo.WrapMiddleware(h.GetAuthMiddleware(domain.RoleRead))) + apiV1.GET("/permissions", h.echoHandler(h.ListPermissions), echo.WrapMiddleware(h.GetAuthMiddleware(domain.PermissionRead))) } } @@ -25,12 +39,3 @@ func (h *Handler) SetupRoutes(engine *echo.Echo) { func (h *Handler) echoHandler(handlerFunc func(w http.ResponseWriter, r *http.Request)) echo.HandlerFunc { return echo.WrapHandler(http.HandlerFunc(handlerFunc)) } - -func (h *Handler) AuthMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Authentication logic here (e.g., check JWT token) - - // If authenticated, proceed to the next handler - next.ServeHTTP(w, r) - }) -} diff --git a/manager/service/auth_svc.go b/manager/service/auth_svc.go index 7871016..be622b6 100644 --- a/manager/service/auth_svc.go +++ b/manager/service/auth_svc.go @@ -3,68 +3,142 @@ package service import ( "context" "fmt" + "net/http" "time" "github.com/Gthulhu/api/manager/domain" - "github.com/Gthulhu/api/pkg/util" + "github.com/Gthulhu/api/manager/errs" "github.com/golang-jwt/jwt/v5" + "github.com/pkg/errors" "go.mongodb.org/mongo-driver/v2/bson" ) -func (svc *Service) SignUp(ctx context.Context, email, password string) error { - creatorID := bson.NewObjectID() - user := domain.User{ - BaseEntity: domain.NewBaseEntity(util.Ptr(creatorID), util.Ptr(creatorID)), - Email: email, +func (svc *Service) CreateNewUser(ctx context.Context, operator *domain.Claims, username, password string) error { + operatorID, err := operator.GetBsonObjectUID() + if err != nil { + return errors.WithMessagef(err, "invalid operator ID %s", operator.UID) + } + user := &domain.User{ + UserName: username, Password: domain.EncryptedPassword(password), Status: domain.UserStatusWaitChangePassword, + BaseEntity: domain.NewBaseEntity(&operatorID, &operatorID), } - err := svc.Repo.CreateUser(ctx, &user) + err = svc.Repo.CreateUser(ctx, user) if err != nil { - // TODO: handle duplicate email error - return err + return errors.WithMessagef(err, "db: create user %s failed", username) } - return nil } -func (svc *Service) Login(ctx context.Context, email, password string) (string, error) { - user, err := svc.getUserByEmaiL(ctx, email) +func (svc *Service) Login(ctx context.Context, username, password string) (string, error) { + user, err := svc.getUserByUserName(ctx, username) if err != nil { return "", err } + if user.Status == domain.UserStatusInactive { + return "", errs.NewHTTPStatusError(http.StatusUnauthorized, "user is inactive", fmt.Errorf("username %s is inactive", username)) + } + ok, err := user.Password.Cmp(password) if err != nil { - return "", err + return "", errors.WithMessagef(err, "compare password for username %s failed", username) } if !ok { - return "", fmt.Errorf("invalid password") + return "", errs.NewHTTPStatusError(http.StatusUnauthorized, "invalid password", fmt.Errorf("compare password for username %s not match", username)) } token, err := svc.genJWTToken(ctx, user) if err != nil { - return "", err + return "", errors.WithMessage(err, "generate JWT token failed") } return token, nil } -func (svc *Service) Logout(ctx context.Context, token string) error { +func (svc *Service) ChangePassword(ctx context.Context, userClaims *domain.Claims, oldPassword, newPassword string) error { + uid, err := userClaims.GetBsonObjectUID() + if err != nil { + return errors.WithMessagef(err, "invalid user ID %s", userClaims.UID) + } + + user, err := svc.getUserByID(ctx, uid) + if err != nil { + return err + } + ok, err := user.Password.Cmp(oldPassword) + if err != nil { + return errors.WithMessagef(err, "compare password for uid %s failed", uid) + } + if !ok { + return errs.NewHTTPStatusError(http.StatusUnauthorized, "invalid password", fmt.Errorf("change password failed, compare password for uid %s not match", uid)) + } + user.Status = domain.UserStatusActive + user.Password = domain.EncryptedPassword(newPassword) + user.UpdatedTime = time.Now().UnixMilli() + user.UpdaterID = uid + err = svc.Repo.UpdateUser(ctx, user) + if err != nil { + return err + } return nil } -func (svc *Service) ChangePassword(ctx context.Context, email string, oldPassword, newPassword string) error { - user, err := svc.getUserByEmaiL(ctx, email) +func (svc *Service) UpdateUserPermissions(ctx context.Context, operator *domain.Claims, id string, opt domain.UpdateUserPermissionsOptions) error { + operatorID, err := operator.GetBsonObjectUID() + if err != nil { + return errors.WithMessagef(err, "invalid operator ID %s", operator.UID) + } + uid, err := bson.ObjectIDFromHex(id) + if err != nil { + return errs.NewHTTPStatusError(http.StatusUnprocessableEntity, "invalid user ID", fmt.Errorf("invalid user ID %s: %v", id, err)) + } + + user, err := svc.getUserByID(ctx, uid) if err != nil { return err } - ok, err := user.Password.Cmp(oldPassword) + if opt.Roles != nil { + query := &domain.QueryRoleOptions{ + Names: *opt.Roles, + } + err = svc.QueryRoles(ctx, query) + if err != nil { + return err + } + if len(*opt.Roles) != len(query.Result) { + return errs.NewHTTPStatusError(http.StatusBadRequest, "Some roles not found", errors.New("invalid role names")) + } + user.Roles = *opt.Roles + } + if opt.Status != nil { + user.Status = *opt.Status + } + user.UpdatedTime = time.Now().UnixMilli() + user.UpdaterID = operatorID + err = svc.Repo.UpdateUser(ctx, user) if err != nil { return err } - if !ok { - return fmt.Errorf("invalid old password") + return nil +} + +func (svc *Service) ResetPassword(ctx context.Context, operator *domain.Claims, id, newPassword string) error { + operatorID, err := operator.GetBsonObjectUID() + if err != nil { + return errors.WithMessagef(err, "invalid operator ID %s", operator.UID) + } + uid, err := bson.ObjectIDFromHex(id) + if err != nil { + return errs.NewHTTPStatusError(http.StatusUnprocessableEntity, "invalid user ID", fmt.Errorf("invalid user ID %s: %v", id, err)) + } + + user, err := svc.getUserByID(ctx, uid) + if err != nil { + return err } user.Password = domain.EncryptedPassword(newPassword) + user.Status = domain.UserStatusWaitChangePassword user.UpdatedTime = time.Now().UnixMilli() + user.UpdaterID = operatorID err = svc.Repo.UpdateUser(ctx, user) if err != nil { return err @@ -72,9 +146,17 @@ func (svc *Service) ChangePassword(ctx context.Context, email string, oldPasswor return nil } -func (svc *Service) getUserByEmaiL(ctx context.Context, email string) (*domain.User, error) { +func (svc *Service) QueryUsers(ctx context.Context, opt *domain.QueryUserOptions) error { + err := svc.Repo.QueryUsers(ctx, opt) + if err != nil { + return err + } + return nil +} + +func (svc *Service) getUserByUserName(ctx context.Context, username string) (*domain.User, error) { opts := &domain.QueryUserOptions{ - Email: email, + UserNames: []string{username}, } err := svc.Repo.QueryUsers(ctx, opts) if err != nil { @@ -82,8 +164,23 @@ func (svc *Service) getUserByEmaiL(ctx context.Context, email string) (*domain.U } users := opts.Result if len(users) == 0 { - // TODO: return specific not found error - return nil, fmt.Errorf("user with email %s not found", email) + return nil, errs.NewHTTPStatusError(http.StatusUnauthorized, "user not found", fmt.Errorf("username %s not found", username)) + } + + return users[0], nil +} + +func (svc *Service) getUserByID(ctx context.Context, id bson.ObjectID) (*domain.User, error) { + opts := &domain.QueryUserOptions{ + IDs: []bson.ObjectID{id}, + } + err := svc.Repo.QueryUsers(ctx, opts) + if err != nil { + return nil, err + } + users := opts.Result + if len(users) == 0 { + return nil, errs.NewHTTPStatusError(http.StatusUnauthorized, "user not found", fmt.Errorf("user ID %s not found", id.Hex())) } return users[0], nil @@ -94,12 +191,12 @@ func (svc *Service) genJWTToken(ctx context.Context, user *domain.User) (string, uid := user.ID.Hex() roles := []string{} - for _, roleID := range user.RoleIDs { - roles = append(roles, roleID.Hex()) + for _, role := range user.Roles { + roles = append(roles, role) } claims := domain.Claims{ - UID: uid, - Roles: roles, + UID: uid, + NeedChangePassword: user.Status == domain.UserStatusWaitChangePassword, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(tokenTTL)), IssuedAt: jwt.NewNumericDate(time.Now()), @@ -112,3 +209,96 @@ func (svc *Service) genJWTToken(ctx context.Context, user *domain.User) (string, token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) return token.SignedString(svc.jwtPrivateKey) } + +func (svc *Service) VerifyJWTToken(ctx context.Context, tokenString string, permissionKey domain.PermissionKey) (domain.Claims, domain.RolePolicy, error) { + token, err := jwt.ParseWithClaims(tokenString, &domain.Claims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return svc.jwtPrivateKey.Public(), nil + }) + if err != nil { + return domain.Claims{}, domain.RolePolicy{}, errors.WithMessage(err, "parse JWT token failed") + } + claims, ok := token.Claims.(*domain.Claims) + if !ok || !token.Valid { + return domain.Claims{}, domain.RolePolicy{}, errors.New("invalid JWT token claims") + } + if permissionKey == "" { + return *claims, domain.RolePolicy{}, nil + } + if permissionKey != domain.ChangeUserPermission && claims.NeedChangePassword { + return domain.Claims{}, domain.RolePolicy{}, errs.NewHTTPStatusError(http.StatusForbidden, "password change required", fmt.Errorf("user %s need to change password", claims.UID)) + } + + uid, err := claims.GetBsonObjectUID() + if err != nil { + return domain.Claims{}, domain.RolePolicy{}, errors.WithMessagef(err, "invalid user ID %s", claims.UID) + } + user, err := svc.getUserByID(ctx, uid) + if err != nil { + return domain.Claims{}, domain.RolePolicy{}, errors.WithMessagef(err, "get user by ID %s failed", uid.Hex()) + } + + roles, err := svc.getRolesByNames(ctx, user.Roles) + if err != nil { + return domain.Claims{}, domain.RolePolicy{}, errors.WithMessage(err, "get roles by IDs failed") + } + if len(roles) == 0 { + return domain.Claims{}, domain.RolePolicy{}, errs.NewHTTPStatusError(http.StatusForbidden, "permission denied", fmt.Errorf("user %s has no roles assigned", claims.UID)) + } + hasPermission := false + rolePolicy := domain.RolePolicy{} + for _, role := range roles { + for _, policy := range role.Policies { + if policy.PermissionKey == permissionKey { + hasPermission = true + rolePolicy = policy + break + } + } + if hasPermission { + break + } + } + if !hasPermission { + return domain.Claims{}, domain.RolePolicy{}, errs.NewHTTPStatusError(http.StatusForbidden, "permission denied", fmt.Errorf("user %s does not have permission %s", claims.UID, permissionKey)) + } + return *claims, rolePolicy, nil +} + +func (svc *Service) CreateAdminUserIfNotExists(ctx context.Context, username, password string) error { + opts := &domain.QueryUserOptions{ + UserNames: []string{username}, + } + err := svc.Repo.QueryUsers(ctx, opts) + if err != nil { + return errors.WithMessagef(err, "db: query user %s failed", username) + } + if len(opts.Result) > 0 { + return nil + } + roleOpts := &domain.QueryRoleOptions{ + Names: []string{domain.AdminRole}, + } + err = svc.QueryRoles(ctx, roleOpts) + if err != nil { + return errors.WithMessagef(err, "db: query admin role failed") + } + if len(roleOpts.Result) == 0 { + return errors.New("admin role not found, please create admin role first") + } + + adminUser := &domain.User{ + UserName: username, + Password: domain.EncryptedPassword(password), + Status: domain.UserStatusActive, + Roles: []string{domain.AdminRole}, + BaseEntity: domain.NewBaseEntity(nil, nil), + } + err = svc.Repo.CreateUser(ctx, adminUser) + if err != nil { + return errors.WithMessagef(err, "db: create admin user %s failed", username) + } + return nil +} diff --git a/manager/service/role_svc.go b/manager/service/role_svc.go new file mode 100644 index 0000000..6a27d12 --- /dev/null +++ b/manager/service/role_svc.go @@ -0,0 +1,102 @@ +package service + +import ( + "context" + "fmt" + "net/http" + + "github.com/Gthulhu/api/manager/domain" + "github.com/Gthulhu/api/manager/errs" + "github.com/pkg/errors" + "go.mongodb.org/mongo-driver/v2/bson" +) + +func (svc *Service) CreateRole(ctx context.Context, operator *domain.Claims, role *domain.Role) error { + operatorID, err := operator.GetBsonObjectUID() + if err != nil { + return errs.NewHTTPStatusError(http.StatusUnauthorized, "unauthorized", fmt.Errorf("invalid user ID")) + } + role.BaseEntity = domain.NewBaseEntity(&operatorID, &operatorID) + return svc.Repo.CreateRole(ctx, role) +} + +func (svc *Service) UpdateRole(ctx context.Context, operator *domain.Claims, roleID string, opt domain.UpdateRoleOptions) error { + operatorID, err := operator.GetBsonObjectUID() + if err != nil { + return errs.NewHTTPStatusError(http.StatusUnauthorized, "unauthorized", fmt.Errorf("invalid user ID")) + } + roles, err := svc.getRolesByIDs(ctx, []string{roleID}) + if err != nil { + return err + } + if len(roles) == 0 { + return errs.NewHTTPStatusError(http.StatusUnprocessableEntity, "role not found", fmt.Errorf("role with ID %s not found", roleID)) + } + role := roles[0] + if opt.Name != nil { + role.Name = *opt.Name + } + if opt.Description != nil { + role.Description = *opt.Description + } + if opt.Policies != nil { + role.Policies = []domain.RolePolicy{} + for _, p := range *opt.Policies { + role.Policies = append(role.Policies, domain.RolePolicy{ + PermissionKey: p.PermissionKey, + Self: p.Self, + K8SNamespace: p.K8SNamespace, + PolicyNamespace: p.PolicyNamespace, + }) + } + } + role.UpdaterID = operatorID + return svc.Repo.UpdateRole(ctx, role) +} + +func (svc *Service) DeleteRole(ctx context.Context, operator *domain.Claims, roleID string) error { + return fmt.Errorf("not implemented") +} + +func (svc *Service) QueryRoles(ctx context.Context, opt *domain.QueryRoleOptions) error { + return svc.Repo.QueryRoles(ctx, opt) +} + +func (svc *Service) getRolesByNames(ctx context.Context, roleNames []string) ([]*domain.Role, error) { + if len(roleNames) == 0 { + return []*domain.Role{}, nil + } + opts := &domain.QueryRoleOptions{ + Names: roleNames, + } + err := svc.Repo.QueryRoles(ctx, opts) + if err != nil { + return nil, err + } + return opts.Result, nil +} +func (svc *Service) getRolesByIDs(ctx context.Context, roleIDs []string) ([]*domain.Role, error) { + if len(roleIDs) == 0 { + return []*domain.Role{}, nil + } + ids := []bson.ObjectID{} + for _, idStr := range roleIDs { + id, err := bson.ObjectIDFromHex(idStr) + if err != nil { + return nil, errors.WithMessagef(err, "invalid role ID %s", idStr) + } + ids = append(ids, id) + } + opts := &domain.QueryRoleOptions{ + IDs: ids, + } + err := svc.Repo.QueryRoles(ctx, opts) + if err != nil { + return nil, err + } + return opts.Result, nil +} + +func (svc Service) QueryPermissions(ctx context.Context, opt *domain.QueryPermissionOptions) error { + return svc.Repo.QueryPermissions(ctx, opt) +} diff --git a/manager/service/svc.go b/manager/service/svc.go index 2f1e74d..908e7fc 100644 --- a/manager/service/svc.go +++ b/manager/service/svc.go @@ -7,6 +7,7 @@ import ( "encoding/pem" "errors" "fmt" + "time" "github.com/Gthulhu/api/config" "github.com/Gthulhu/api/manager/domain" @@ -21,7 +22,7 @@ type Params struct { } func NewService(params Params) (domain.Service, error) { - jwtPrivateKey, err := initRSAPrivateKey(params.KeyConfig.RsaPrivateKeyPem) + jwtPrivateKey, err := initRSAPrivateKey(string(params.KeyConfig.RsaPrivateKeyPem)) if err != nil { return nil, fmt.Errorf("initialize RSA private key: %w", err) } @@ -31,6 +32,13 @@ func NewService(params Params) (domain.Service, error) { jwtPrivateKey: jwtPrivateKey, } + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + err = svc.CreateAdminUserIfNotExists(ctx, params.AccountConfig.AdminEmail, params.AccountConfig.AdminPassword.Value()) + if err != nil { + return nil, fmt.Errorf("create admin user if not exists: %w", err) + } + return svc, nil } @@ -64,21 +72,3 @@ func initRSAPrivateKey(pemStr string) (*rsa.PrivateKey, error) { func (svc Service) ListAuditLogs(ctx context.Context, opt *domain.QueryAuditLogOptions) error { return errors.New("not implemented") } -func (svc Service) CreateRole(ctx context.Context, role *domain.Role) error { - return errors.New("not implemented") -} -func (svc Service) UpdateRole(ctx context.Context, role *domain.Role) error { - return errors.New("not implemented") -} -func (svc Service) QueryRoles(ctx context.Context, opt *domain.QueryRoleOptions) error { - return errors.New("not implemented") -} -func (svc Service) CreatePermission(ctx context.Context, permission *domain.Permission) error { - return errors.New("not implemented") -} -func (svc Service) UpdatePermission(ctx context.Context, permission *domain.Permission) error { - return errors.New("not implemented") -} -func (svc Service) QueryPermissions(ctx context.Context, opt *domain.QueryPermissionOptions) error { - return errors.New("not implemented") -} diff --git a/pkg/container/mongo_container.go b/pkg/container/mongo_container.go index 9d4d943..960d2cf 100644 --- a/pkg/container/mongo_container.go +++ b/pkg/container/mongo_container.go @@ -1,11 +1,15 @@ package container import ( + "context" "fmt" "strconv" + "time" "github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3/docker" + mongo "go.mongodb.org/mongo-driver/v2/mongo" + mongooption "go.mongodb.org/mongo-driver/v2/mongo/options" ) type MongoContainerOptions struct { @@ -82,12 +86,27 @@ func RunMongoContainer(builder *ContainerBuilder, name string, options MongoCont if err != nil { return MongoContainerConnection{}, err } + builder.AddContainer(resource.Container.ID, ContainerInfo{ Name: name, Type: ContainerTypeMongoDB, }) host := resource.GetBoundIP(strconv.Itoa(mongoDBPort) + "/tcp") mongoPort := resource.GetPort(strconv.Itoa(mongoDBPort) + "/tcp") + + builder.Retry(func() error { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + uri := fmt.Sprintf("mongodb://%s:%s@%s:%s", options.Username, options.Password, host, mongoPort) + + mongoOpts := mongooption.Client().ApplyURI(uri) + client, err := mongo.Connect(mongoOpts) + if err != nil { + return err + } + return client.Ping(ctx, nil) + }) + return MongoContainerConnection{ Host: host, Port: mongoPort, diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 80044d5..0044ad5 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -13,6 +13,7 @@ func InitLogger() *zerolog.Logger { logger := zerolog.New(consoleWriter). With(). Timestamp(). + Caller(). Logger() zerolog.SetGlobalLevel(zerolog.DebugLevel) zerolog.DefaultContextLogger = &logger diff --git a/pkg/util/dbclean.go b/pkg/util/dbclean.go new file mode 100644 index 0000000..cc84710 --- /dev/null +++ b/pkg/util/dbclean.go @@ -0,0 +1,7 @@ +package util + +import "go.mongodb.org/mongo-driver/v2/mongo" + +func MongoCleanup(mongodbClient *mongo.Client, dbName string) error { + return mongodbClient.Database(dbName).Drop(nil) +} diff --git a/pkg/util/encrypt.go b/pkg/util/encrypt.go index ce5e533..d86360b 100644 --- a/pkg/util/encrypt.go +++ b/pkg/util/encrypt.go @@ -5,6 +5,7 @@ import ( "crypto/subtle" "encoding/base64" "fmt" + "regexp" "strings" "golang.org/x/crypto/argon2" @@ -30,6 +31,12 @@ func InitArgon2idParams(param Argon2idParams) { defaultArgon2idParams = param } +var argon2Regex = regexp.MustCompile(`^\$argon2(id|i|d)\$v=\d+\$m=\d+,t=\d+,p=\d+\$[A-Za-z0-9+/=]+\$[A-Za-z0-9+/=]+$`) + +func IsArgon2Hash(s string) bool { + return argon2Regex.MatchString(s) +} + func CreateArgon2Hash(password string) (string, error) { // 1. 產生隨機 Salt p := defaultArgon2idParams diff --git a/pkg/util/encrypt_test.go b/pkg/util/encrypt_test.go index 0dbb6f2..9437be4 100644 --- a/pkg/util/encrypt_test.go +++ b/pkg/util/encrypt_test.go @@ -8,11 +8,13 @@ import ( ) func TestAngron(t *testing.T) { - password := "my_secure_password" + password := "your-password-here" hash, err := CreateArgon2Hash(password) require.NoError(t, err) + t.Log(string(hash)) + ok, err := ComparePasswordAndHash(password, hash) require.NoError(t, err) assert.True(t, ok, "Password should match the hash") diff --git a/pkg/util/logger.go b/pkg/util/logger.go deleted file mode 100644 index d42bf5c..0000000 --- a/pkg/util/logger.go +++ /dev/null @@ -1,24 +0,0 @@ -package util - -import ( - "context" - "os" - - "github.com/rs/zerolog" -) - -func InitLogger() *zerolog.Logger { - consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: "15:04:05"} - - logger := zerolog.New(consoleWriter). - With(). - Timestamp(). - Logger() - zerolog.SetGlobalLevel(zerolog.DebugLevel) - zerolog.DefaultContextLogger = &logger - return &logger -} - -func Logger(ctx context.Context) *zerolog.Logger { - return zerolog.Ctx(ctx) -} From d1e1d63d123ac0688e93f4906034b40571f0e872 Mon Sep 17 00:00:00 2001 From: Yanun Date: Sun, 30 Nov 2025 21:46:46 +0800 Subject: [PATCH 09/18] account/rbac_repo: mongo test --- manager/repository/repo.go | 6 -- manager/repository/repo_test.go | 148 ++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 manager/repository/repo_test.go diff --git a/manager/repository/repo.go b/manager/repository/repo.go index a937e1f..663c741 100644 --- a/manager/repository/repo.go +++ b/manager/repository/repo.go @@ -216,14 +216,9 @@ func (r *repo) CreatePermission(ctx context.Context, permission *domain.Permissi return errors.New("nil permission") } - // now := time.Now().UnixMilli() if permission.ID.IsZero() { permission.ID = bson.NewObjectID() } - // if permission.CreatedTime == 0 { - // permission.CreatedTime = now - // } - // permission.UpdatedTime = now res, err := r.db.Collection(permissionCollection).InsertOne(ctx, permission) if err != nil { @@ -243,7 +238,6 @@ func (r *repo) UpdatePermission(ctx context.Context, permission *domain.Permissi return errors.New("permission id is required") } - // permission.UpdatedTime = time.Now().UnixMilli() res, err := r.db.Collection(permissionCollection).ReplaceOne(ctx, bson.M{"_id": permission.ID}, permission) if err != nil { return fmt.Errorf("update permission, err: %w", err) diff --git a/manager/repository/repo_test.go b/manager/repository/repo_test.go new file mode 100644 index 0000000..d22a02e --- /dev/null +++ b/manager/repository/repo_test.go @@ -0,0 +1,148 @@ +package repository + +import ( + "context" + "testing" + + "github.com/Gthulhu/api/config" + "github.com/Gthulhu/api/manager/domain" + "github.com/Gthulhu/api/pkg/container" + "github.com/Gthulhu/api/pkg/logger" + "github.com/Gthulhu/api/pkg/util" + "github.com/stretchr/testify/suite" + "go.mongodb.org/mongo-driver/v2/bson" +) + +func TestRepositoryTestSuite(t *testing.T) { + suite.Run(t, new(RepositoryTestSuite)) +} + +type RepositoryTestSuite struct { + suite.Suite + ctx context.Context + repo *repo + containerBuild *container.ContainerBuilder + mongoCfg config.MongoDBConfig +} + +func (suite *RepositoryTestSuite) SetupSuite() { + logger.InitLogger() + suite.ctx = context.Background() + + builder, err := container.NewContainerBuilder("") + suite.Require().NoError(err, "init container builder") + suite.containerBuild = builder + + cfg, err := config.InitManagerConfig("manager_config.test.toml", config.GetAbsPath("config")) + suite.Require().NoError(err, "load test config") + + conn, err := container.RunMongoContainer(builder, "api_repo_test_mongo", container.MongoContainerConnection{ + Username: cfg.MongoDB.User, + Password: string(cfg.MongoDB.Password), + Database: cfg.MongoDB.Database, + Port: cfg.MongoDB.Port, + }) + suite.Require().NoError(err, "start mongo container") + + cfg.MongoDB.Host = conn.Host + cfg.MongoDB.Port = conn.Port + cfg.MongoDB.User = conn.Username + cfg.MongoDB.Password = config.SecretValue(conn.Password) + cfg.MongoDB.Database = conn.Database + suite.mongoCfg = cfg.MongoDB + + repoInst, err := NewRepository(Params{MongoConfig: cfg.MongoDB}) + suite.Require().NoError(err, "init repository") + + r, ok := repoInst.(*repo) + suite.Require().True(ok, "repository type assertion") + suite.repo = r +} + +func (suite *RepositoryTestSuite) TearDownSuite() { + if suite.containerBuild != nil { + err := suite.containerBuild.PruneAll() + suite.Require().NoError(err, "prune containers") + } +} + +func (suite *RepositoryTestSuite) SetupTest() { + suite.Require().NotNil(suite.repo, "repository not initialized") + err := util.MongoCleanup(suite.repo.client, suite.mongoCfg.Database) + suite.Require().NoError(err, "cleanup database") +} + +func (suite *RepositoryTestSuite) TestCreateAndQueryUser() { + user := &domain.User{ + UserName: "test-user", + Password: domain.EncryptedPassword("secret"), + Status: domain.UserStatusActive, + } + err := suite.repo.CreateUser(suite.ctx, user) + suite.Require().NoError(err, "create user") + suite.NotZero(user.ID, "user id should be assigned") + + opts := &domain.QueryUserOptions{ + UserNames: []string{user.UserName}, + } + err = suite.repo.QueryUsers(suite.ctx, opts) + suite.Require().NoError(err, "query users") + suite.Len(opts.Result, 1, "expect one user") + suite.Equal(user.UserName, opts.Result[0].UserName, "username should match") +} + +func (suite *RepositoryTestSuite) TestUpdateUserStatus() { + user := &domain.User{ + UserName: "update-user", + Password: domain.EncryptedPassword("secret"), + Status: domain.UserStatusActive, + } + err := suite.repo.CreateUser(suite.ctx, user) + suite.Require().NoError(err, "create user") + + user.Status = domain.UserStatusInactive + err = suite.repo.UpdateUser(suite.ctx, user) + suite.Require().NoError(err, "update user") + + opts := &domain.QueryUserOptions{IDs: []bson.ObjectID{user.ID}} + err = suite.repo.QueryUsers(suite.ctx, opts) + suite.Require().NoError(err, "query users by id") + suite.Len(opts.Result, 1, "expect one user after update") + suite.Equal(domain.UserStatusInactive, opts.Result[0].Status, "status should be updated") +} + +func (suite *RepositoryTestSuite) TestCreateRoleAndPermission() { + role := &domain.Role{ + Name: "viewer", + Description: "view only", + Policies: []domain.RolePolicy{ + { + PermissionKey: domain.PermissionRead, + Self: true, + }, + }, + } + err := suite.repo.CreateRole(suite.ctx, role) + suite.Require().NoError(err, "create role") + + roleOpts := &domain.QueryRoleOptions{Names: []string{role.Name}} + err = suite.repo.QueryRoles(suite.ctx, roleOpts) + suite.Require().NoError(err, "query roles") + suite.Len(roleOpts.Result, 1, "expect one role") + suite.Equal(role.Name, roleOpts.Result[0].Name, "role name should match") + + perm := &domain.Permission{ + Key: domain.PermissionRead, + Description: "read access", + Resource: "resource", + Action: domain.PermissionActionRead, + } + err = suite.repo.CreatePermission(suite.ctx, perm) + suite.Require().NoError(err, "create permission") + + permOpts := &domain.QueryPermissionOptions{Keys: []string{string(perm.Key)}} + err = suite.repo.QueryPermissions(suite.ctx, permOpts) + suite.Require().NoError(err, "query permissions") + suite.Len(permOpts.Result, 1, "expect one permission") + suite.Equal(perm.Description, permOpts.Result[0].Description, "permission description should match") +} From d170a6f35c5a92817279af83cfbfa0552972f6a1 Mon Sep 17 00:00:00 2001 From: Yanun Date: Sun, 30 Nov 2025 21:46:58 +0800 Subject: [PATCH 10/18] account/rbac_repo: swag doc --- docs/docs.go | 1097 ++++++++++++++++++++++++++++++++++++++ docs/swagger.json | 1068 +++++++++++++++++++++++++++++++++++++ docs/swagger.yaml | 688 ++++++++++++++++++++++++ go.mod | 10 + go.sum | 37 ++ manager/rest/auth_hdl.go | 87 +++ manager/rest/hander.go | 47 +- manager/rest/role_hdl.go | 62 +++ manager/rest/swagger.go | 10 + 9 files changed, 3098 insertions(+), 8 deletions(-) create mode 100644 docs/docs.go create mode 100644 docs/swagger.json create mode 100644 docs/swagger.yaml create mode 100644 manager/rest/swagger.go diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..8540770 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,1097 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/v1/auth/login": { + "post": { + "description": "Authenticate user and return JWT token.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "User login", + "parameters": [ + { + "description": "Login payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rest.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_LoginResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + } + }, + "/api/v1/permissions": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve all permission keys.", + "produces": [ + "application/json" + ], + "tags": [ + "Roles" + ], + "summary": "List permissions", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_ListPermissionsResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + } + }, + "/api/v1/roles": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve available roles.", + "produces": [ + "application/json" + ], + "tags": [ + "Roles" + ], + "summary": "List roles", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_ListRolesResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update role information or policies.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Roles" + ], + "summary": "Update role", + "parameters": [ + { + "description": "Role payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rest.UpdateRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a new role with policies.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Roles" + ], + "summary": "Create role", + "parameters": [ + { + "description": "Role payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rest.CreateRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete a role by ID.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Roles" + ], + "summary": "Delete role", + "parameters": [ + { + "description": "Role payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rest.DeleteRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + } + }, + "/api/v1/users": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve user list.", + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "List users", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_ListUsersResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a new user account.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Create user", + "parameters": [ + { + "description": "User payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rest.CreateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + } + }, + "/api/v1/users/password": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Reset another user's password.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Reset user password", + "parameters": [ + { + "description": "Reset payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rest.ResetPasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + } + }, + "/api/v1/users/permissions": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update a user's roles or status.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Update user roles and status", + "parameters": [ + { + "description": "Permissions payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rest.UpdateUserPermissionsRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + } + }, + "/api/v1/users/self": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve profile of the authenticated user.", + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Get current user", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_GetSelfUserResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + } + }, + "/api/v1/users/self/password": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update current user's password.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Change own password", + "parameters": [ + { + "description": "Password payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rest.ChangePasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + } + }, + "/health": { + "get": { + "description": "Basic health check for readiness probes.", + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "Health check", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.HealthResponse" + } + } + } + } + }, + "/version": { + "get": { + "description": "Returns service version and exposed endpoints.", + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "Get service version", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.VersionResponse" + } + } + } + } + } + }, + "definitions": { + "domain.PermissionKey": { + "type": "string", + "enum": [ + "user.create", + "user.read", + "user.permission.update", + "user.password.reset", + "role.create", + "role.read", + "role.update", + "role.delete", + "permission.read" + ], + "x-enum-varnames": [ + "CreateUser", + "UserRead", + "ChangeUserPermission", + "ResetUserPassword", + "RoleCrete", + "RoleRead", + "RoleUpdate", + "RoleDelete", + "PermissionRead" + ] + }, + "domain.UserStatus": { + "type": "integer", + "format": "int32", + "enum": [ + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "UserStatusActive", + "UserStatusInactive", + "UserStatusWaitChangePassword" + ] + }, + "rest.ChangePasswordRequest": { + "type": "object", + "properties": { + "newPassword": { + "type": "string" + }, + "oldPassword": { + "type": "string" + } + } + }, + "rest.CreateRoleRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "rolePolicies": { + "type": "array", + "items": { + "$ref": "#/definitions/rest.RolePolicy" + } + } + } + }, + "rest.CreateUserRequest": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "rest.DeleteRoleRequest": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + }, + "rest.EmptyResponse": { + "type": "object" + }, + "rest.ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + }, + "rest.GetSelfUserResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "$ref": "#/definitions/domain.UserStatus" + }, + "username": { + "type": "string" + } + } + }, + "rest.HealthResponse": { + "type": "object", + "properties": { + "service": { + "type": "string" + }, + "status": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, + "rest.ListPermissionsResponse": { + "type": "object", + "properties": { + "permissions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "key": { + "$ref": "#/definitions/domain.PermissionKey" + } + } + } + } + } + }, + "rest.ListRolesResponse": { + "type": "object", + "properties": { + "roles": { + "type": "array", + "items": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "rolePolicy": { + "type": "array", + "items": { + "$ref": "#/definitions/rest.RolePolicy" + } + } + } + } + } + } + }, + "rest.ListUsersResponse": { + "type": "object", + "properties": { + "users": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "$ref": "#/definitions/domain.UserStatus" + }, + "username": { + "type": "string" + } + } + } + } + } + }, + "rest.LoginRequest": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "rest.LoginResponse": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, + "rest.ResetPasswordRequest": { + "type": "object", + "properties": { + "newPassword": { + "type": "string" + }, + "userID": { + "type": "string" + } + } + }, + "rest.RolePolicy": { + "type": "object", + "properties": { + "k8sNamespace": { + "type": "string" + }, + "permissionKey": { + "$ref": "#/definitions/domain.PermissionKey" + }, + "policyNamespace": { + "type": "string" + }, + "self": { + "type": "boolean" + } + } + }, + "rest.SuccessResponse-rest_EmptyResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.EmptyResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "rest.SuccessResponse-rest_GetSelfUserResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.GetSelfUserResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "rest.SuccessResponse-rest_ListPermissionsResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.ListPermissionsResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "rest.SuccessResponse-rest_ListRolesResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.ListRolesResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "rest.SuccessResponse-rest_ListUsersResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.ListUsersResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "rest.SuccessResponse-rest_LoginResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.LoginResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "rest.UpdateRoleRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "rolePolicy": { + "type": "array", + "items": { + "$ref": "#/definitions/rest.RolePolicy" + } + } + } + }, + "rest.UpdateUserPermissionsRequest": { + "type": "object", + "properties": { + "roles": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "$ref": "#/definitions/domain.UserStatus" + }, + "userID": { + "type": "string" + } + } + }, + "rest.VersionResponse": { + "type": "object", + "properties": { + "endpoints": { + "type": "string" + }, + "message": { + "type": "string" + }, + "version": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "", + Description: "", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..dde18b0 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,1068 @@ +{ + "swagger": "2.0", + "info": { + "contact": {} + }, + "paths": { + "/api/v1/auth/login": { + "post": { + "description": "Authenticate user and return JWT token.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "User login", + "parameters": [ + { + "description": "Login payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rest.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_LoginResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + } + }, + "/api/v1/permissions": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve all permission keys.", + "produces": [ + "application/json" + ], + "tags": [ + "Roles" + ], + "summary": "List permissions", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_ListPermissionsResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + } + }, + "/api/v1/roles": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve available roles.", + "produces": [ + "application/json" + ], + "tags": [ + "Roles" + ], + "summary": "List roles", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_ListRolesResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update role information or policies.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Roles" + ], + "summary": "Update role", + "parameters": [ + { + "description": "Role payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rest.UpdateRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a new role with policies.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Roles" + ], + "summary": "Create role", + "parameters": [ + { + "description": "Role payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rest.CreateRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete a role by ID.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Roles" + ], + "summary": "Delete role", + "parameters": [ + { + "description": "Role payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rest.DeleteRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + } + }, + "/api/v1/users": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve user list.", + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "List users", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_ListUsersResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a new user account.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Create user", + "parameters": [ + { + "description": "User payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rest.CreateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + } + }, + "/api/v1/users/password": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Reset another user's password.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Reset user password", + "parameters": [ + { + "description": "Reset payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rest.ResetPasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + } + }, + "/api/v1/users/permissions": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update a user's roles or status.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Update user roles and status", + "parameters": [ + { + "description": "Permissions payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rest.UpdateUserPermissionsRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + } + }, + "/api/v1/users/self": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve profile of the authenticated user.", + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Get current user", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_GetSelfUserResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + } + }, + "/api/v1/users/self/password": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update current user's password.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Change own password", + "parameters": [ + { + "description": "Password payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rest.ChangePasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/rest.ErrorResponse" + } + } + } + } + }, + "/health": { + "get": { + "description": "Basic health check for readiness probes.", + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "Health check", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.HealthResponse" + } + } + } + } + }, + "/version": { + "get": { + "description": "Returns service version and exposed endpoints.", + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "Get service version", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/rest.VersionResponse" + } + } + } + } + } + }, + "definitions": { + "domain.PermissionKey": { + "type": "string", + "enum": [ + "user.create", + "user.read", + "user.permission.update", + "user.password.reset", + "role.create", + "role.read", + "role.update", + "role.delete", + "permission.read" + ], + "x-enum-varnames": [ + "CreateUser", + "UserRead", + "ChangeUserPermission", + "ResetUserPassword", + "RoleCrete", + "RoleRead", + "RoleUpdate", + "RoleDelete", + "PermissionRead" + ] + }, + "domain.UserStatus": { + "type": "integer", + "format": "int32", + "enum": [ + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "UserStatusActive", + "UserStatusInactive", + "UserStatusWaitChangePassword" + ] + }, + "rest.ChangePasswordRequest": { + "type": "object", + "properties": { + "newPassword": { + "type": "string" + }, + "oldPassword": { + "type": "string" + } + } + }, + "rest.CreateRoleRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "rolePolicies": { + "type": "array", + "items": { + "$ref": "#/definitions/rest.RolePolicy" + } + } + } + }, + "rest.CreateUserRequest": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "rest.DeleteRoleRequest": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + }, + "rest.EmptyResponse": { + "type": "object" + }, + "rest.ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + }, + "rest.GetSelfUserResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "$ref": "#/definitions/domain.UserStatus" + }, + "username": { + "type": "string" + } + } + }, + "rest.HealthResponse": { + "type": "object", + "properties": { + "service": { + "type": "string" + }, + "status": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, + "rest.ListPermissionsResponse": { + "type": "object", + "properties": { + "permissions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "key": { + "$ref": "#/definitions/domain.PermissionKey" + } + } + } + } + } + }, + "rest.ListRolesResponse": { + "type": "object", + "properties": { + "roles": { + "type": "array", + "items": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "rolePolicy": { + "type": "array", + "items": { + "$ref": "#/definitions/rest.RolePolicy" + } + } + } + } + } + } + }, + "rest.ListUsersResponse": { + "type": "object", + "properties": { + "users": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "$ref": "#/definitions/domain.UserStatus" + }, + "username": { + "type": "string" + } + } + } + } + } + }, + "rest.LoginRequest": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "rest.LoginResponse": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, + "rest.ResetPasswordRequest": { + "type": "object", + "properties": { + "newPassword": { + "type": "string" + }, + "userID": { + "type": "string" + } + } + }, + "rest.RolePolicy": { + "type": "object", + "properties": { + "k8sNamespace": { + "type": "string" + }, + "permissionKey": { + "$ref": "#/definitions/domain.PermissionKey" + }, + "policyNamespace": { + "type": "string" + }, + "self": { + "type": "boolean" + } + } + }, + "rest.SuccessResponse-rest_EmptyResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.EmptyResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "rest.SuccessResponse-rest_GetSelfUserResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.GetSelfUserResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "rest.SuccessResponse-rest_ListPermissionsResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.ListPermissionsResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "rest.SuccessResponse-rest_ListRolesResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.ListRolesResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "rest.SuccessResponse-rest_ListUsersResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.ListUsersResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "rest.SuccessResponse-rest_LoginResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.LoginResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "rest.UpdateRoleRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "rolePolicy": { + "type": "array", + "items": { + "$ref": "#/definitions/rest.RolePolicy" + } + } + } + }, + "rest.UpdateUserPermissionsRequest": { + "type": "object", + "properties": { + "roles": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "$ref": "#/definitions/domain.UserStatus" + }, + "userID": { + "type": "string" + } + } + }, + "rest.VersionResponse": { + "type": "object", + "properties": { + "endpoints": { + "type": "string" + }, + "message": { + "type": "string" + }, + "version": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..c9481c3 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,688 @@ +definitions: + domain.PermissionKey: + enum: + - user.create + - user.read + - user.permission.update + - user.password.reset + - role.create + - role.read + - role.update + - role.delete + - permission.read + type: string + x-enum-varnames: + - CreateUser + - UserRead + - ChangeUserPermission + - ResetUserPassword + - RoleCrete + - RoleRead + - RoleUpdate + - RoleDelete + - PermissionRead + domain.UserStatus: + enum: + - 1 + - 2 + - 3 + format: int32 + type: integer + x-enum-varnames: + - UserStatusActive + - UserStatusInactive + - UserStatusWaitChangePassword + rest.ChangePasswordRequest: + properties: + newPassword: + type: string + oldPassword: + type: string + type: object + rest.CreateRoleRequest: + properties: + description: + type: string + name: + type: string + rolePolicies: + items: + $ref: '#/definitions/rest.RolePolicy' + type: array + type: object + rest.CreateUserRequest: + properties: + password: + type: string + username: + type: string + type: object + rest.DeleteRoleRequest: + properties: + id: + type: string + type: object + rest.EmptyResponse: + type: object + rest.ErrorResponse: + properties: + error: + type: string + success: + type: boolean + type: object + rest.GetSelfUserResponse: + properties: + id: + type: string + roles: + items: + type: string + type: array + status: + $ref: '#/definitions/domain.UserStatus' + username: + type: string + type: object + rest.HealthResponse: + properties: + service: + type: string + status: + type: string + timestamp: + type: string + type: object + rest.ListPermissionsResponse: + properties: + permissions: + items: + properties: + description: + type: string + key: + $ref: '#/definitions/domain.PermissionKey' + type: object + type: array + type: object + rest.ListRolesResponse: + properties: + roles: + items: + properties: + description: + type: string + id: + type: string + name: + type: string + rolePolicy: + items: + $ref: '#/definitions/rest.RolePolicy' + type: array + type: object + type: array + type: object + rest.ListUsersResponse: + properties: + users: + items: + properties: + id: + type: string + roles: + items: + type: string + type: array + status: + $ref: '#/definitions/domain.UserStatus' + username: + type: string + type: object + type: array + type: object + rest.LoginRequest: + properties: + password: + type: string + username: + type: string + type: object + rest.LoginResponse: + properties: + token: + type: string + type: object + rest.ResetPasswordRequest: + properties: + newPassword: + type: string + userID: + type: string + type: object + rest.RolePolicy: + properties: + k8sNamespace: + type: string + permissionKey: + $ref: '#/definitions/domain.PermissionKey' + policyNamespace: + type: string + self: + type: boolean + type: object + rest.SuccessResponse-rest_EmptyResponse: + properties: + data: + $ref: '#/definitions/rest.EmptyResponse' + success: + type: boolean + timestamp: + type: string + type: object + rest.SuccessResponse-rest_GetSelfUserResponse: + properties: + data: + $ref: '#/definitions/rest.GetSelfUserResponse' + success: + type: boolean + timestamp: + type: string + type: object + rest.SuccessResponse-rest_ListPermissionsResponse: + properties: + data: + $ref: '#/definitions/rest.ListPermissionsResponse' + success: + type: boolean + timestamp: + type: string + type: object + rest.SuccessResponse-rest_ListRolesResponse: + properties: + data: + $ref: '#/definitions/rest.ListRolesResponse' + success: + type: boolean + timestamp: + type: string + type: object + rest.SuccessResponse-rest_ListUsersResponse: + properties: + data: + $ref: '#/definitions/rest.ListUsersResponse' + success: + type: boolean + timestamp: + type: string + type: object + rest.SuccessResponse-rest_LoginResponse: + properties: + data: + $ref: '#/definitions/rest.LoginResponse' + success: + type: boolean + timestamp: + type: string + type: object + rest.UpdateRoleRequest: + properties: + description: + type: string + id: + type: string + name: + type: string + rolePolicy: + items: + $ref: '#/definitions/rest.RolePolicy' + type: array + type: object + rest.UpdateUserPermissionsRequest: + properties: + roles: + items: + type: string + type: array + status: + $ref: '#/definitions/domain.UserStatus' + userID: + type: string + type: object + rest.VersionResponse: + properties: + endpoints: + type: string + message: + type: string + version: + type: string + type: object +info: + contact: {} +paths: + /api/v1/auth/login: + post: + consumes: + - application/json + description: Authenticate user and return JWT token. + parameters: + - description: Login payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/rest.LoginRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/rest.SuccessResponse-rest_LoginResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/rest.ErrorResponse' + "422": + description: Unprocessable Entity + schema: + $ref: '#/definitions/rest.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/rest.ErrorResponse' + summary: User login + tags: + - Auth + /api/v1/permissions: + get: + description: Retrieve all permission keys. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/rest.SuccessResponse-rest_ListPermissionsResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/rest.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/rest.ErrorResponse' + security: + - BearerAuth: [] + summary: List permissions + tags: + - Roles + /api/v1/roles: + delete: + consumes: + - application/json + description: Delete a role by ID. + parameters: + - description: Role payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/rest.DeleteRoleRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/rest.SuccessResponse-rest_EmptyResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/rest.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/rest.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/rest.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/rest.ErrorResponse' + security: + - BearerAuth: [] + summary: Delete role + tags: + - Roles + get: + description: Retrieve available roles. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/rest.SuccessResponse-rest_ListRolesResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/rest.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/rest.ErrorResponse' + security: + - BearerAuth: [] + summary: List roles + tags: + - Roles + post: + consumes: + - application/json + description: Create a new role with policies. + parameters: + - description: Role payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/rest.CreateRoleRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/rest.SuccessResponse-rest_EmptyResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/rest.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/rest.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/rest.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/rest.ErrorResponse' + security: + - BearerAuth: [] + summary: Create role + tags: + - Roles + put: + consumes: + - application/json + description: Update role information or policies. + parameters: + - description: Role payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/rest.UpdateRoleRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/rest.SuccessResponse-rest_EmptyResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/rest.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/rest.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/rest.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/rest.ErrorResponse' + security: + - BearerAuth: [] + summary: Update role + tags: + - Roles + /api/v1/users: + get: + description: Retrieve user list. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/rest.SuccessResponse-rest_ListUsersResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/rest.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/rest.ErrorResponse' + security: + - BearerAuth: [] + summary: List users + tags: + - Users + post: + consumes: + - application/json + description: Create a new user account. + parameters: + - description: User payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/rest.CreateUserRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/rest.SuccessResponse-rest_EmptyResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/rest.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/rest.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/rest.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/rest.ErrorResponse' + security: + - BearerAuth: [] + summary: Create user + tags: + - Users + /api/v1/users/password: + put: + consumes: + - application/json + description: Reset another user's password. + parameters: + - description: Reset payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/rest.ResetPasswordRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/rest.SuccessResponse-rest_EmptyResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/rest.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/rest.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/rest.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/rest.ErrorResponse' + security: + - BearerAuth: [] + summary: Reset user password + tags: + - Users + /api/v1/users/permissions: + put: + consumes: + - application/json + description: Update a user's roles or status. + parameters: + - description: Permissions payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/rest.UpdateUserPermissionsRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/rest.SuccessResponse-rest_EmptyResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/rest.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/rest.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/rest.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/rest.ErrorResponse' + security: + - BearerAuth: [] + summary: Update user roles and status + tags: + - Users + /api/v1/users/self: + get: + description: Retrieve profile of the authenticated user. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/rest.SuccessResponse-rest_GetSelfUserResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/rest.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/rest.ErrorResponse' + security: + - BearerAuth: [] + summary: Get current user + tags: + - Users + /api/v1/users/self/password: + put: + consumes: + - application/json + description: Update current user's password. + parameters: + - description: Password payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/rest.ChangePasswordRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/rest.SuccessResponse-rest_EmptyResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/rest.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/rest.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/rest.ErrorResponse' + security: + - BearerAuth: [] + summary: Change own password + tags: + - Users + /health: + get: + description: Basic health check for readiness probes. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/rest.HealthResponse' + summary: Health check + tags: + - System + /version: + get: + description: Returns service version and exposed endpoints. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/rest.VersionResponse' + summary: Get service version + tags: + - System +swagger: "2.0" diff --git a/go.mod b/go.mod index d4946bc..100ad66 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 + github.com/swaggo/swag v1.16.6 go.mongodb.org/mongo-driver v1.17.6 go.mongodb.org/mongo-driver/v2 v2.4.0 go.uber.org/fx v1.24.0 @@ -26,8 +27,11 @@ require ( require ( dario.cat/mergo v1.0.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/brunoga/deep v1.2.4 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/containerd/continuity v0.4.5 // indirect @@ -38,6 +42,10 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/snappy v1.0.0 // indirect @@ -46,6 +54,7 @@ require ( github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jedib0t/go-pretty/v6 v6.6.7 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.16.7 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect github.com/knadh/koanf/parsers/yaml v0.1.0 // indirect @@ -55,6 +64,7 @@ require ( github.com/knadh/koanf/providers/structs v0.1.0 // indirect github.com/knadh/koanf/v2 v2.3.0 // indirect github.com/labstack/gommon v0.4.2 // indirect + github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect diff --git a/go.sum b/go.sum index 8f01be7..32d25e3 100644 --- a/go.sum +++ b/go.sum @@ -4,10 +4,16 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/brunoga/deep v1.2.4 h1:Aj9E9oUbE+ccbyh35VC/NHlzzjfIVU69BXu2mt2LmL8= github.com/brunoga/deep v1.2.4/go.mod h1:GDV6dnXqn80ezsLSZ5Wlv1PdKAWAO4L5PnKYtv2dgaI= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -20,6 +26,7 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -50,6 +57,16 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= @@ -73,6 +90,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 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= @@ -91,8 +110,11 @@ github.com/knadh/koanf/providers/structs v0.1.0 h1:wJRteCNn1qvLtE5h8KQBvLJovidSd github.com/knadh/koanf/providers/structs v0.1.0/go.mod h1:sw2YZ3txUcqA3Z27gPlmmBzWn1h8Nt9O6EP/91MkcWE= github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM= github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= @@ -101,6 +123,10 @@ github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0 github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= @@ -124,6 +150,7 @@ github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8 github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -170,11 +197,14 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= @@ -243,6 +273,7 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= @@ -256,6 +287,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/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-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -272,6 +304,7 @@ golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= 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.6/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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= @@ -288,11 +321,15 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/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.0-20200615113413-eeeca48fe776/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= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= diff --git a/manager/rest/auth_hdl.go b/manager/rest/auth_hdl.go index 66a9b0c..cd29bde 100644 --- a/manager/rest/auth_hdl.go +++ b/manager/rest/auth_hdl.go @@ -13,6 +13,20 @@ type CreateUserRequest struct { Password string `json:"password"` } +// CreateUser godoc +// @Summary Create user +// @Description Create a new user account. +// @Tags Users +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body CreateUserRequest true "User payload" +// @Success 200 {object} SuccessResponse[EmptyResponse] +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /api/v1/users [post] func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req CreateUserRequest @@ -47,6 +61,18 @@ type LoginResponse struct { Token string `json:"token"` } +// Login godoc +// @Summary User login +// @Description Authenticate user and return JWT token. +// @Tags Auth +// @Accept json +// @Produce json +// @Param request body LoginRequest true "Login payload" +// @Success 200 {object} SuccessResponse[LoginResponse] +// @Failure 400 {object} ErrorResponse +// @Failure 422 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /api/v1/auth/login [post] func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req LoginRequest @@ -77,6 +103,19 @@ type ChangePasswordRequest struct { NewPassword string `json:"newPassword"` } +// ChangePassword godoc +// @Summary Change own password +// @Description Update current user's password. +// @Tags Users +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body ChangePasswordRequest true "Password payload" +// @Success 200 {object} SuccessResponse[EmptyResponse] +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /api/v1/users/self/password [put] func (h *Handler) ChangePassword(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req ChangePasswordRequest @@ -111,6 +150,20 @@ type ResetPasswordRequest struct { NewPassword string `json:"newPassword"` } +// ResetPassword godoc +// @Summary Reset user password +// @Description Reset another user's password. +// @Tags Users +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body ResetPasswordRequest true "Reset payload" +// @Success 200 {object} SuccessResponse[EmptyResponse] +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /api/v1/users/password [put] func (h *Handler) ResetPassword(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req ResetPasswordRequest @@ -147,6 +200,20 @@ type UpdateUserPermissionsRequest struct { Status *domain.UserStatus `json:"status,omitempty"` } +// UpdateUserPermissions godoc +// @Summary Update user roles and status +// @Description Update a user's roles or status. +// @Tags Users +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body UpdateUserPermissionsRequest true "Permissions payload" +// @Success 200 {object} SuccessResponse[EmptyResponse] +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /api/v1/users/permissions [put] func (h *Handler) UpdateUserPermissions(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req UpdateUserPermissionsRequest @@ -186,6 +253,16 @@ type ListUsersResponse struct { } `json:"users"` } +// ListUsers godoc +// @Summary List users +// @Description Retrieve user list. +// @Tags Users +// @Produce json +// @Security BearerAuth +// @Success 200 {object} SuccessResponse[ListUsersResponse] +// @Failure 401 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /api/v1/users [get] func (h *Handler) ListUsers(w http.ResponseWriter, r *http.Request) { ctx := r.Context() query := domain.QueryUserOptions{} @@ -222,6 +299,16 @@ type GetSelfUserResponse struct { Status domain.UserStatus `json:"status"` } +// GetSelfUser godoc +// @Summary Get current user +// @Description Retrieve profile of the authenticated user. +// @Tags Users +// @Produce json +// @Security BearerAuth +// @Success 200 {object} SuccessResponse[GetSelfUserResponse] +// @Failure 401 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /api/v1/users/self [get] func (h *Handler) GetSelfUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context() claims, ok := h.GetClaimsFromContext(ctx) diff --git a/manager/rest/hander.go b/manager/rest/hander.go index e26d105..0f31178 100644 --- a/manager/rest/hander.go +++ b/manager/rest/hander.go @@ -19,6 +19,23 @@ type ErrorResponse struct { Error string `json:"error"` } +// EmptyResponse is used for endpoints that return no data payload. +type EmptyResponse struct{} + +// VersionResponse describes the version endpoint payload. +type VersionResponse struct { + Message string `json:"message"` + Version string `json:"version"` + Endpoints string `json:"endpoints"` +} + +// HealthResponse describes the health check payload. +type HealthResponse struct { + Status string `json:"status"` + Timestamp string `json:"timestamp"` + Service string `json:"service"` +} + func NewSuccessResponse[T any](data *T) SuccessResponse[T] { return SuccessResponse[T]{ Success: true, @@ -92,20 +109,34 @@ func (h *Handler) ErrorResponse(ctx context.Context, w http.ResponseWriter, stat h.JSONResponse(ctx, w, status, resp) } +// Version godoc +// @Summary Get service version +// @Description Returns service version and exposed endpoints. +// @Tags System +// @Produce json +// @Success 200 {object} VersionResponse +// @Router /version [get] func (h *Handler) Version(w http.ResponseWriter, r *http.Request) { - response := map[string]string{ - "message": "BSS Metrics API Server", - "version": "1.0.0", - "endpoints": "/api/v1/auth/token (POST), /api/v1/metrics (POST), /api/v1/pods/pids (GET), /api/v1/scheduling/strategies (GET, POST), /health (GET), /static/ (Frontend)", + response := VersionResponse{ + Message: "BSS Metrics API Server", + Version: "1.0.0", + Endpoints: "/api/v1/auth/token (POST), /api/v1/metrics (POST), /api/v1/pods/pids (GET), /api/v1/scheduling/strategies (GET, POST), /health (GET), /static/ (Frontend)", } h.JSONResponse(r.Context(), w, http.StatusOK, response) } +// HealthCheck godoc +// @Summary Health check +// @Description Basic health check for readiness probes. +// @Tags System +// @Produce json +// @Success 200 {object} HealthResponse +// @Router /health [get] func (h *Handler) HealthCheck(w http.ResponseWriter, r *http.Request) { - response := map[string]any{ - "status": "healthy", - "timestamp": time.Now().UTC().Format(time.RFC3339), - "service": "BSS Metrics API Server", + response := HealthResponse{ + Status: "healthy", + Timestamp: time.Now().UTC().Format(time.RFC3339), + Service: "BSS Metrics API Server", } h.JSONResponse(r.Context(), w, http.StatusOK, response) } diff --git a/manager/rest/role_hdl.go b/manager/rest/role_hdl.go index 61263b9..0983cc2 100644 --- a/manager/rest/role_hdl.go +++ b/manager/rest/role_hdl.go @@ -20,6 +20,20 @@ type CreateRoleRequest struct { RolePolicies []RolePolicy `json:"rolePolicies"` } +// CreateRole godoc +// @Summary Create role +// @Description Create a new role with policies. +// @Tags Roles +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body CreateRoleRequest true "Role payload" +// @Success 200 {object} SuccessResponse[EmptyResponse] +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /api/v1/roles [post] func (h *Handler) CreateRole(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req CreateRoleRequest @@ -64,6 +78,20 @@ type UpdateRoleRequest struct { RolePolicy *[]RolePolicy `json:"rolePolicy,omitempty"` } +// UpdateRole godoc +// @Summary Update role +// @Description Update role information or policies. +// @Tags Roles +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body UpdateRoleRequest true "Role payload" +// @Success 200 {object} SuccessResponse[EmptyResponse] +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /api/v1/roles [put] func (h *Handler) UpdateRole(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req UpdateRoleRequest @@ -113,6 +141,20 @@ type DeleteRoleRequest struct { ID string `json:"id"` } +// DeleteRole godoc +// @Summary Delete role +// @Description Delete a role by ID. +// @Tags Roles +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body DeleteRoleRequest true "Role payload" +// @Success 200 {object} SuccessResponse[EmptyResponse] +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /api/v1/roles [delete] func (h *Handler) DeleteRole(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req DeleteRoleRequest @@ -147,6 +189,16 @@ type ListRolesResponse struct { } `json:"roles"` } +// ListRoles godoc +// @Summary List roles +// @Description Retrieve available roles. +// @Tags Roles +// @Produce json +// @Security BearerAuth +// @Success 200 {object} SuccessResponse[ListRolesResponse] +// @Failure 401 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /api/v1/roles [get] func (h *Handler) ListRoles(w http.ResponseWriter, r *http.Request) { ctx := r.Context() queryOpts := &domain.QueryRoleOptions{} @@ -191,6 +243,16 @@ type ListPermissionsResponse struct { } `json:"permissions"` } +// ListPermissions godoc +// @Summary List permissions +// @Description Retrieve all permission keys. +// @Tags Roles +// @Produce json +// @Security BearerAuth +// @Success 200 {object} SuccessResponse[ListPermissionsResponse] +// @Failure 401 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /api/v1/permissions [get] func (h *Handler) ListPermissions(w http.ResponseWriter, r *http.Request) { ctx := r.Context() queryOpts := &domain.QueryPermissionOptions{} diff --git a/manager/rest/swagger.go b/manager/rest/swagger.go new file mode 100644 index 0000000..e03020a --- /dev/null +++ b/manager/rest/swagger.go @@ -0,0 +1,10 @@ +package rest + +// @title Manager API +// @version 1.0 +// @description API documentation for Manager service. +// @BasePath / +// @securityDefinitions.apikey BearerAuth +// @in header +// @name Authorization +// @description Provide token as "Bearer {token}" From df2ce79c4b53d8fe6e6c811d60eb1bbb29952f1d Mon Sep 17 00:00:00 2001 From: Yanun Date: Sun, 30 Nov 2025 21:59:32 +0800 Subject: [PATCH 11/18] account/rbac_repo: change mongo test port to avoid conflict --- manager/errs/errors.go | 2 +- manager/repository/repo_test.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/manager/errs/errors.go b/manager/errs/errors.go index bcf77b7..e6df3f4 100644 --- a/manager/errs/errors.go +++ b/manager/errs/errors.go @@ -13,7 +13,7 @@ type HTTPStatusError struct { } func (e *HTTPStatusError) Error() string { - return fmt.Sprintf("(status %d) %s: %w", e.StatusCode, e.Message, e.OriginalErr) + return fmt.Sprintf("(status %d) %s: %v", e.StatusCode, e.Message, e.OriginalErr) } func NewHTTPStatusError(statusCode int, message string, originalErr error) *HTTPStatusError { diff --git a/manager/repository/repo_test.go b/manager/repository/repo_test.go index d22a02e..57c7fe9 100644 --- a/manager/repository/repo_test.go +++ b/manager/repository/repo_test.go @@ -35,6 +35,7 @@ func (suite *RepositoryTestSuite) SetupSuite() { cfg, err := config.InitManagerConfig("manager_config.test.toml", config.GetAbsPath("config")) suite.Require().NoError(err, "load test config") + cfg.MongoDB.Port = "27018" conn, err := container.RunMongoContainer(builder, "api_repo_test_mongo", container.MongoContainerConnection{ Username: cfg.MongoDB.User, From 3d0f14fb7b0e3b3854f081b0cd6fd066b680c9ae Mon Sep 17 00:00:00 2001 From: Yanun Date: Mon, 1 Dec 2025 21:08:54 +0800 Subject: [PATCH 12/18] account/rbac_repo: add swagger restful endpoint --- go.mod | 5 ++++- go.sum | 9 ++++++++- manager/rest/routes.go | 4 ++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 100ad66..f6c9c9b 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 + github.com/swaggo/echo-swagger v1.4.1 github.com/swaggo/swag v1.16.6 go.mongodb.org/mongo-driver v1.17.6 go.mongodb.org/mongo-driver/v2 v2.4.0 @@ -42,6 +43,7 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/ghodss/yaml v1.0.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.6 // indirect github.com/go-openapi/spec v0.20.4 // indirect @@ -64,7 +66,7 @@ require ( github.com/knadh/koanf/providers/structs v0.1.0 // indirect github.com/knadh/koanf/v2 v2.3.0 // indirect github.com/labstack/gommon v0.4.2 // indirect - github.com/mailru/easyjson v0.7.6 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect @@ -88,6 +90,7 @@ require ( github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/swaggo/files/v2 v2.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect github.com/vektra/mockery/v3 v3.5.5 // indirect diff --git a/go.sum b/go.sum index 32d25e3..4dea7aa 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -125,8 +127,9 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= @@ -203,6 +206,10 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk= +github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc= +github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= +github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= diff --git a/manager/rest/routes.go b/manager/rest/routes.go index 4c49699..5b38e05 100644 --- a/manager/rest/routes.go +++ b/manager/rest/routes.go @@ -3,13 +3,17 @@ package rest import ( "net/http" + "github.com/Gthulhu/api/docs" "github.com/Gthulhu/api/manager/domain" "github.com/labstack/echo/v4" + echoSwagger "github.com/swaggo/echo-swagger" ) func (h *Handler) SetupRoutes(engine *echo.Echo) { engine.GET("/health", h.echoHandler(h.HealthCheck)) engine.GET("/version", h.echoHandler(h.Version)) + docs.SwaggerInfo.BasePath = "/" + engine.GET("/swagger/*", echoSwagger.WrapHandler) api := engine.Group("/api", echo.WrapMiddleware(LoggerMiddleware)) // v1 routes From 2dc7dc2eb95db3740c8a127f266530d7fbdfb86f Mon Sep 17 00:00:00 2001 From: Vic Xu Date: Thu, 4 Dec 2025 08:39:19 +0800 Subject: [PATCH 13/18] strategy: define strategy related model and interface --- manager/domain/enums.go | 16 ++++++++++++++++ manager/domain/interface.go | 19 +++++++++++++++++++ manager/domain/k8s_resource.go | 22 ++++++++++++++++++++++ manager/domain/strategy.go | 33 +++++++++++++++++++++++++++++++++ manager/k8s_adapter/init | 0 5 files changed, 90 insertions(+) create mode 100644 manager/domain/k8s_resource.go create mode 100644 manager/domain/strategy.go create mode 100644 manager/k8s_adapter/init diff --git a/manager/domain/enums.go b/manager/domain/enums.go index 645b788..f5cba61 100644 --- a/manager/domain/enums.go +++ b/manager/domain/enums.go @@ -17,3 +17,19 @@ const ( const ( AdminRole = "admin" ) + +type NodeState int8 + +const ( + NodeStateUnknown NodeState = iota + NodeStateOnline + NodeStateOffline +) + +type IntentState int8 + +const ( + IntentStateUnknown IntentState = iota + IntentStateInitialized + IntentStateSent +) diff --git a/manager/domain/interface.go b/manager/domain/interface.go index 980adaf..8d0ca58 100644 --- a/manager/domain/interface.go +++ b/manager/domain/interface.go @@ -64,3 +64,22 @@ type Service interface { ListAuditLogs(ctx context.Context, opt *QueryAuditLogOptions) error } + +type QueryPodsOptions struct { + K8SNamespace []string + LabelSelectors []LabelSelector + CommandRegex string + Result []*Pod +} + +type QueryDecisionMakerPodsOptions struct { + K8SNamespace []string + NodeIDs []string + DecisionMakerLabel LabelSelector + Result []*DecisionMakerPod +} + +type K8SAdapter interface { + QueryPods(ctx context.Context, opt *QueryPodsOptions) error + QueryDecisionMakerPods(ctx context.Context, opt *QueryDecisionMakerPodsOptions) error +} diff --git a/manager/domain/k8s_resource.go b/manager/domain/k8s_resource.go new file mode 100644 index 0000000..1c3c6f8 --- /dev/null +++ b/manager/domain/k8s_resource.go @@ -0,0 +1,22 @@ +package domain + +type DecisionMakerPod struct { + NodeID string + Port int + Host string + State NodeState +} + +type Pod struct { + K8SNamespace string + Labels map[string]string + PodID string + NodeID string + Containers []Container +} + +type Container struct { + ContainerID string + Name string + Command []string +} diff --git a/manager/domain/strategy.go b/manager/domain/strategy.go new file mode 100644 index 0000000..70ac948 --- /dev/null +++ b/manager/domain/strategy.go @@ -0,0 +1,33 @@ +package domain + +import "go.mongodb.org/mongo-driver/v2/bson" + +type ScheduleStrategy struct { + BaseEntity + StrategyNamespace string `bson:"strategyNamespace,omitempty"` + LabelSelectors []LabelSelector `bson:"labelSelectors,omitempty"` + K8sNamespaces []string `bson:"k8sNamespaces,omitempty"` + CommandRegex string `bson:"commandRegex,omitempty"` + Priority int `bson:"priority,omitempty"` + ExecutionTime int64 `bson:"executionTime,omitempty"` +} + +type ScheduleIntent struct { + ID bson.ObjectID `bson:"_id,omitempty"` + StrategyID bson.ObjectID `bson:"strategyID,omitempty"` + PodID string `bson:"podID,omitempty"` + NodeID string `bson:"nodeID,omitempty"` + K8sNamespace string `bson:"k8sNamespace,omitempty"` + CommandRegex string `bson:"commandRegex,omitempty"` + Priority int `bson:"priority,omitempty"` + ExecutionTime int64 `bson:"executionTime,omitempty"` + LabelSelectors []LabelSelector `bson:"labelSelectors,omitempty"` + State IntentState `bson:"state,omitempty"` + CreatedTime int64 `bson:"createdTime,omitempty"` + SentTime int64 `bson:"sentTime,omitempty"` +} + +type LabelSelector struct { + Key string `bson:"key,omitempty"` + Value string `bson:"value,omitempty"` +} diff --git a/manager/k8s_adapter/init b/manager/k8s_adapter/init new file mode 100644 index 0000000..e69de29 From 422e8fb1fccfe32283a2fc652f74d12666d385bb Mon Sep 17 00:00:00 2001 From: Yanun Date: Tue, 9 Dec 2025 02:11:51 +0800 Subject: [PATCH 14/18] strategy/base_impl_k8s_adapter: implement k8s adapter --- .gitignore | 16 +- go.mod | 36 ++- go.sum | 83 ++++- manager/domain/errors.go | 5 +- manager/k8s_adapter/adapter.go | 372 ++++++++++++++++++++++ manager/k8s_adapter/adapter_local_test.go | 299 +++++++++++++++++ manager/k8s_adapter/adapter_test.go | 280 ++++++++++++++++ manager/k8s_adapter/init | 0 8 files changed, 1078 insertions(+), 13 deletions(-) create mode 100644 manager/k8s_adapter/adapter.go create mode 100644 manager/k8s_adapter/adapter_local_test.go create mode 100644 manager/k8s_adapter/adapter_test.go delete mode 100644 manager/k8s_adapter/init diff --git a/.gitignore b/.gitignore index e41b991..594a4ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,17 @@ +# Mac system files +*.DS_Store +*.DS_STORE + +# VSCode settings +*.vscode + +# Log +*.log + +# Others +*.build + bin api -config/manager_config.toml \ No newline at end of file +config/manager_config.toml +*.ref \ No newline at end of file diff --git a/go.mod b/go.mod index f6c9c9b..755f3c9 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,9 @@ require ( go.mongodb.org/mongo-driver/v2 v2.4.0 go.uber.org/fx v1.24.0 golang.org/x/crypto v0.45.0 + k8s.io/api v0.31.0 + k8s.io/apimachinery v0.31.0 + k8s.io/client-go v0.31.0 ) require ( @@ -31,8 +34,6 @@ require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect - github.com/PuerkitoBio/purell v1.1.1 // indirect - github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/brunoga/deep v1.2.4 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/containerd/continuity v0.4.5 // indirect @@ -41,22 +42,31 @@ require ( github.com/docker/docker v28.3.3+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/spec v0.20.4 // indirect - github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-openapi/swag v0.22.4 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect + github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect + github.com/imdario/mergo v0.3.6 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jedib0t/go-pretty/v6 v6.6.7 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.16.7 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect github.com/knadh/koanf/parsers/yaml v0.1.0 // indirect @@ -75,7 +85,10 @@ require ( github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/sys/user v0.3.0 // indirect github.com/moby/term v0.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/montanaflynn/stats v0.7.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/runc v1.2.3 // indirect @@ -94,6 +107,7 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect github.com/vektra/mockery/v3 v3.5.5 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect @@ -102,18 +116,28 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect go.uber.org/dig v1.19.0 // indirect - go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.26.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.29.0 // indirect golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect + golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.38.0 // indirect + google.golang.org/protobuf v1.36.7 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + 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/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 4dea7aa..ec23dd5 100644 --- a/go.sum +++ b/go.sum @@ -10,9 +10,7 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= -github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/brunoga/deep v1.2.4 h1:Aj9E9oUbE+ccbyh35VC/NHlzzjfIVU69BXu2mt2LmL8= github.com/brunoga/deep v1.2.4/go.mod h1:GDV6dnXqn80ezsLSZ5Wlv1PdKAWAO4L5PnKYtv2dgaI= @@ -45,6 +43,8 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +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/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -53,6 +53,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -60,17 +62,23 @@ github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -80,20 +88,36 @@ github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9v github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA= github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +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= @@ -113,6 +137,7 @@ github.com/knadh/koanf/providers/structs v0.1.0/go.mod h1:sw2YZ3txUcqA3Z27gPlmmB github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM= github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= 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= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -149,11 +174,23 @@ github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +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= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -197,11 +234,16 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -218,6 +260,8 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vektra/mockery/v3 v3.5.5 h1:1ExE+yqz3ytvEOe7pUH5VWIwmsYlSq+FjWPVVLdE8O4= github.com/vektra/mockery/v3 v3.5.5/go.mod h1:Oti3Df0WP8wwT31yuVri3QNsDeMUQU5Q4QEg8EabaBw= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -284,6 +328,8 @@ golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 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= @@ -316,6 +362,8 @@ 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -327,12 +375,19 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= +google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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= @@ -341,3 +396,21 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo= +k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE= +k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc= +k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8= +k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/manager/domain/errors.go b/manager/domain/errors.go index d6db21e..48ee2c7 100644 --- a/manager/domain/errors.go +++ b/manager/domain/errors.go @@ -3,5 +3,8 @@ package domain import "errors" var ( - ErrNotFound = errors.New("not found") + ErrNotFound = errors.New("not found") + ErrNoKubeConfig = errors.New("kubernetes configuration not provided") + ErrNilQueryInput = errors.New("query options is nil") + ErrNoClient = errors.New("kubernetes client is not initialized") ) diff --git a/manager/k8s_adapter/adapter.go b/manager/k8s_adapter/adapter.go new file mode 100644 index 0000000..d545d90 --- /dev/null +++ b/manager/k8s_adapter/adapter.go @@ -0,0 +1,372 @@ +package k8sadapter + +import ( + "context" + "fmt" + "regexp" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/Gthulhu/api/manager/domain" + apiv1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/clientcmd" +) + +var ( + _ domain.K8SAdapter = (*Adapter)(nil) +) + +type Options struct { + KubeConfigPath string + InCluster bool +} + +type Adapter struct { + client kubernetes.Interface + podCache map[string]apiv1.Pod + podCacheMu sync.RWMutex + stopCh chan struct{} + startWatcher sync.Once + stopWatcher sync.Once + cacheHasSynced atomic.Bool +} + +func NewAdapter(opt Options) (*Adapter, error) { + config, err := buildConfig(opt) + if err != nil { + return nil, err + } + + config.Timeout = 10 * time.Second + config.QPS = 20 + config.Burst = 50 + + client, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("create kubernetes client: %w", err) + } + + adapter := &Adapter{ + client: client, + podCache: make(map[string]apiv1.Pod), + stopCh: make(chan struct{}), + } + adapter.startPodWatcher() + + return adapter, nil +} + +func buildConfig(opt Options) (*rest.Config, error) { + if opt.InCluster { + cfg, err := rest.InClusterConfig() + if err != nil { + return nil, fmt.Errorf("build in-cluster config: %w", err) + } + return cfg, nil + } + + if opt.KubeConfigPath == "" { + return nil, domain.ErrNoKubeConfig + } + + cfg, err := clientcmd.BuildConfigFromFlags("", opt.KubeConfigPath) + if err != nil { + return nil, fmt.Errorf("build kubeconfig from %s: %w", opt.KubeConfigPath, err) + } + + return cfg, nil +} + +func (a *Adapter) startPodWatcher() { + a.startWatcher.Do(func() { + informerFactory := informers.NewSharedInformerFactory(a.client, 0) + podInformer := informerFactory.Core().V1().Pods().Informer() + + podInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + pod, ok := obj.(*apiv1.Pod) + if !ok { + return + } + a.setPodCache(*pod) + }, + UpdateFunc: func(_, newObj interface{}) { + pod, ok := newObj.(*apiv1.Pod) + if !ok { + return + } + a.setPodCache(*pod) + }, + DeleteFunc: func(obj interface{}) { + switch pod := obj.(type) { + case *apiv1.Pod: + a.deletePodCache(string(pod.UID)) + case cache.DeletedFinalStateUnknown: + if p, ok := pod.Obj.(*apiv1.Pod); ok { + a.deletePodCache(string(p.UID)) + } + } + }, + }) + + informerFactory.Start(a.stopCh) + + synced := cache.WaitForCacheSync(a.stopCh, podInformer.HasSynced) + a.cacheHasSynced.Store(synced) + }) +} + +func (a *Adapter) StopPodWatcher() { + a.stopWatcher.Do(func() { + if a.stopCh != nil { + close(a.stopCh) + } + }) +} + +func (a *Adapter) QueryPods(ctx context.Context, opt *domain.QueryPodsOptions) error { + if opt == nil { + return domain.ErrNilQueryInput + } + if a == nil || a.client == nil { + return domain.ErrNoClient + } + + opt.Result = opt.Result[:0] + + labelSelector := buildLabelSelector(opt.LabelSelectors) + namespaces := opt.K8SNamespace + if len(namespaces) == 0 { + namespaces = []string{metav1.NamespaceAll} + } + + var cmdRegex *regexp.Regexp + if opt.CommandRegex != "" { + re, err := regexp.Compile(opt.CommandRegex) + if err != nil { + return fmt.Errorf("compile command regex: %w", err) + } + cmdRegex = re + } + + pods, err := a.listPods(ctx, namespaces, labelSelector) + if err != nil { + return err + } + + for _, pod := range pods { + containers := buildContainers(pod, cmdRegex) + if cmdRegex != nil && len(containers) == 0 { + continue + } + + podLabels := copyLabels(pod.Labels) + result := &domain.Pod{ + K8SNamespace: pod.Namespace, + Labels: podLabels, + PodID: string(pod.UID), + NodeID: pod.Spec.NodeName, + Containers: containers, + } + opt.Result = append(opt.Result, result) + } + + return nil +} + +func (a *Adapter) QueryDecisionMakerPods(ctx context.Context, opt *domain.QueryDecisionMakerPodsOptions) error { + if opt == nil { + return domain.ErrNilQueryInput + } + + if a == nil || a.client == nil { + return domain.ErrNoClient + } + + opt.Result = opt.Result[:0] + + labelSelector := buildLabelSelector([]domain.LabelSelector{opt.DecisionMakerLabel}) + namespaces := opt.K8SNamespace + if len(namespaces) == 0 { + namespaces = []string{metav1.NamespaceAll} + } + + nodeFilters := make(map[string]struct{}, len(opt.NodeIDs)) + for _, id := range opt.NodeIDs { + nodeFilters[id] = struct{}{} + } + + pods, err := a.listPods(ctx, namespaces, labelSelector) + if err != nil { + return err + } + + for _, pod := range pods { + if len(nodeFilters) > 0 { + if _, ok := nodeFilters[pod.Spec.NodeName]; !ok { + continue + } + } + + host := pod.Status.PodIP + if host == "" { + host = pod.Status.HostIP + } + + opt.Result = append(opt.Result, &domain.DecisionMakerPod{ + NodeID: pod.Spec.NodeName, + Port: firstContainerPort(pod), + Host: host, + State: mapPodState(pod.Status.Phase), + }) + } + + return nil +} + +func (a *Adapter) listPods(ctx context.Context, namespaces []string, labelSelector string) ([]apiv1.Pod, error) { + selector, err := labels.Parse(labelSelector) + if err != nil { + return nil, fmt.Errorf("parse label selector %q: %w", labelSelector, err) + } + + if a.cacheHasSynced.Load() { + return a.podsFromCache(namespaces, selector), nil + } + + return a.listPodsLive(ctx, namespaces, labelSelector) +} + +func (a *Adapter) podsFromCache(namespaces []string, selector labels.Selector) []apiv1.Pod { + nsAll := len(namespaces) == 1 && namespaces[0] == metav1.NamespaceAll + nsSet := make(map[string]struct{}, len(namespaces)) + for _, ns := range namespaces { + nsSet[ns] = struct{}{} + } + + a.podCacheMu.RLock() + defer a.podCacheMu.RUnlock() + + pods := make([]apiv1.Pod, 0, len(a.podCache)) + for _, pod := range a.podCache { + if !nsAll { + if _, ok := nsSet[pod.Namespace]; !ok { + continue + } + } + if selector.String() != "" && !selector.Matches(labels.Set(pod.Labels)) { + continue + } + pods = append(pods, pod) + } + return pods +} + +func (a *Adapter) listPodsLive(ctx context.Context, namespaces []string, labelSelector string) ([]apiv1.Pod, error) { + results := make([]apiv1.Pod, 0) + for _, ns := range namespaces { + pods, err := a.client.CoreV1().Pods(ns).List(ctx, metav1.ListOptions{ + LabelSelector: labelSelector, + }) + if err != nil { + return nil, fmt.Errorf("list pods in namespace %s: %w", ns, err) + } + for _, pod := range pods.Items { + a.setPodCache(pod) + results = append(results, pod) + } + } + return results, nil +} + +func (a *Adapter) setPodCache(pod apiv1.Pod) { + a.podCacheMu.Lock() + a.podCache[string(pod.UID)] = pod + a.podCacheMu.Unlock() +} + +func (a *Adapter) deletePodCache(uid string) { + a.podCacheMu.Lock() + delete(a.podCache, uid) + a.podCacheMu.Unlock() +} + +func buildLabelSelector(selectors []domain.LabelSelector) string { + labels := make([]string, 0, len(selectors)) + for _, selector := range selectors { + if selector.Key == "" { + continue + } + if selector.Value == "" { + labels = append(labels, selector.Key) + continue + } + labels = append(labels, fmt.Sprintf("%s=%s", selector.Key, selector.Value)) + } + return strings.Join(labels, ",") +} + +func buildContainers(pod apiv1.Pod, cmdRegex *regexp.Regexp) []domain.Container { + statusByName := make(map[string]string, len(pod.Status.ContainerStatuses)) + for _, status := range pod.Status.ContainerStatuses { + statusByName[status.Name] = status.ContainerID + } + + result := make([]domain.Container, 0, len(pod.Spec.Containers)) + for _, container := range pod.Spec.Containers { + command := append([]string{}, container.Command...) + command = append(command, container.Args...) + + if cmdRegex != nil && !cmdRegex.MatchString(strings.Join(command, " ")) { + continue + } + + result = append(result, domain.Container{ + ContainerID: statusByName[container.Name], + Name: container.Name, + Command: command, + }) + } + return result +} + +func copyLabels(labels map[string]string) map[string]string { + if len(labels) == 0 { + return nil + } + + cloned := make(map[string]string, len(labels)) + for k, v := range labels { + cloned[k] = v + } + return cloned +} + +func firstContainerPort(pod apiv1.Pod) int { + for _, container := range pod.Spec.Containers { + if len(container.Ports) == 0 { + continue + } + return int(container.Ports[0].ContainerPort) + } + return 0 +} + +func mapPodState(phase apiv1.PodPhase) domain.NodeState { + switch phase { + case apiv1.PodRunning: + return domain.NodeStateOnline + case apiv1.PodPending: + return domain.NodeStateUnknown + default: + return domain.NodeStateOffline + } +} diff --git a/manager/k8s_adapter/adapter_local_test.go b/manager/k8s_adapter/adapter_local_test.go new file mode 100644 index 0000000..9fe03ab --- /dev/null +++ b/manager/k8s_adapter/adapter_local_test.go @@ -0,0 +1,299 @@ +/* +For local testing, use `k3d`. + +Install `k3d` with one of the following: + - curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash + - brew install k3d +*/ +package k8sadapter + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "github.com/Gthulhu/api/manager/domain" + apiv1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" +) + +/* +Using `k3d` for local testing. To install k3d, please checkout the package doc top. +*/ +func tryGetKubeConfigPathFromK3d(t *testing.T) (string, func()) { + t.Helper() + + k3dPath, err := exec.LookPath("k3d") + if err != nil { + t.Skip("k3d not installed; skip integration test") + } + + t.Logf("k3d founded: %s", k3dPath) + + clusterName := fmt.Sprintf("api-adapter-%d", time.Now().UnixNano()) + _ = exec.Command(k3dPath, "cluster", "delete", clusterName).Run() + + createCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + createCmd := exec.CommandContext(createCtx, k3dPath, "cluster", "create", clusterName, "--wait") + if out, err := createCmd.CombinedOutput(); err != nil { + t.Skipf("failed to create k3d cluster: %v, output: %s", err, string(out)) + } + + cleanupFunc := func() { exec.Command(k3dPath, "cluster", "delete", clusterName).Run() } + + kubeconfigPath := filepath.Join(t.TempDir(), "kubeconfig") + kubeconfigCmd := exec.Command(k3dPath, "kubeconfig", "get", clusterName) + kubeconfigOut, err := kubeconfigCmd.Output() + if err != nil { + t.Skipf("failed to get kubeconfig: %v", err) + } + + if err := os.WriteFile(kubeconfigPath, kubeconfigOut, 0o600); err != nil { + t.Skipf("failed to write kubeconfig: %v", err) + } + + return kubeconfigPath, cleanupFunc +} + +func TestQueryPodsWithLocalKubeconfig(t *testing.T) { + t.Parallel() + + kubeconfigPath, cleanupK3d := tryGetKubeConfigPathFromK3d(t) + defer cleanupK3d() + + adapter, err := NewAdapter(Options{ + KubeConfigPath: kubeconfigPath, + }) + if err != nil { + t.Skipf("cannot initialize adapter with kubeconfig %s: %v", kubeconfigPath, err) + } + defer adapter.StopPodWatcher() + + cfg, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath) + if err != nil { + t.Skipf("failed to build config: %v", err) + } + client, err := kubernetes.NewForConfig(cfg) + if err != nil { + t.Skipf("failed to create clientset: %v", err) + } + + ns := "default" + pod := &apiv1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "adapter-local-test", + Namespace: ns, + Labels: map[string]string{ + "app": "demo", + }, + }, + Spec: apiv1.PodSpec{ + Containers: []apiv1.Container{ + { + Name: "pause", + Image: "registry.k8s.io/pause:3.9", + Command: []string{"/pause"}, + }, + }, + }, + } + + { /*** Test Creating Pod ***/ + created, err := client.CoreV1().Pods(ns).Create(context.Background(), pod, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("failed to create pod: %v", err) + } + t.Cleanup(func() { + _ = client.CoreV1().Pods(ns).Delete(context.Background(), created.Name, metav1.DeleteOptions{}) + }) + + opt := &domain.QueryPodsOptions{ + K8SNamespace: []string{metav1.NamespaceAll}, + LabelSelectors: []domain.LabelSelector{ + {Key: "app", Value: "demo"}, + }, + } + + waitForLocal(t, func(ctx context.Context) (bool, error) { + opt.Result = opt.Result[:0] + if err := adapter.QueryPods(ctx, opt); err != nil { + return false, err + } + return len(opt.Result) == 1, nil + }) + + { /*** Test Modifying Pod ***/ + if err := retryUpdatePodLabel(context.Background(), client, ns, created.Name, "app", "demo2"); err != nil { + t.Fatalf("failed to update pod: %v", err) + } + + opt.LabelSelectors = []domain.LabelSelector{{Key: "app", Value: "demo2"}} + waitForLocal(t, func(ctx context.Context) (bool, error) { + opt.Result = opt.Result[:0] + if err := adapter.QueryPods(ctx, opt); err != nil { + return false, err + } + return len(opt.Result) == 1 && opt.Result[0].Labels["app"] == "demo2", nil + }) + } + + { /*** Test Deleting Pod ***/ + if err := client.CoreV1().Pods(ns).Delete(context.Background(), created.Name, metav1.DeleteOptions{}); err != nil { + t.Fatalf("failed to delete pod: %v", err) + } + + waitForLocal(t, func(ctx context.Context) (bool, error) { + opt.Result = opt.Result[:0] + if err := adapter.QueryPods(ctx, opt); err != nil { + return false, err + } + return len(opt.Result) == 0, nil + }) + } + } +} + +func waitForLocal(t *testing.T, cond func(ctx context.Context) (bool, error)) { + t.Helper() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := wait.PollUntilContextTimeout(ctx, 200*time.Millisecond, 30*time.Second, false, func(ctx context.Context) (bool, error) { + return cond(ctx) + }); err != nil { + t.Fatalf("condition not met: %v", err) + } +} + +func retryUpdatePodLabel(ctx context.Context, client *kubernetes.Clientset, ns, name, key, val string) error { + return wait.PollUntilContextTimeout(ctx, 200*time.Millisecond, 10*time.Second, false, func(ctx context.Context) (bool, error) { + p, err := client.CoreV1().Pods(ns).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return false, err + } + if p.Labels == nil { + p.Labels = make(map[string]string) + } + p.Labels[key] = val + _, err = client.CoreV1().Pods(ns).Update(ctx, p, metav1.UpdateOptions{}) + if err != nil { + return false, nil + } + return true, nil + }) +} + +func TestQueryDecisionMakerPodsWithLocalKubeconfig(t *testing.T) { + t.Parallel() + + kubeconfigPath, cleanupK3d := tryGetKubeConfigPathFromK3d(t) + defer cleanupK3d() + + adapter, err := NewAdapter(Options{ + KubeConfigPath: kubeconfigPath, + }) + if err != nil { + t.Skipf("cannot initialize adapter with kubeconfig %s: %v", kubeconfigPath, err) + } + defer adapter.StopPodWatcher() + + cfg, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath) + if err != nil { + t.Skipf("failed to build config: %v", err) + } + client, err := kubernetes.NewForConfig(cfg) + if err != nil { + t.Skipf("failed to create clientset: %v", err) + } + + ns := "default" + pod := &apiv1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "adapter-local-dm-test", + Namespace: ns, + Labels: map[string]string{ + "dm": "true", + }, + }, + Spec: apiv1.PodSpec{ + Containers: []apiv1.Container{ + { + Name: "pause", + Image: "registry.k8s.io/pause:3.9", + Ports: []apiv1.ContainerPort{{ContainerPort: 8080}}, + }, + }, + }, + } + + { /*** Test Creating DecisionMaker Pod ***/ + created, err := client.CoreV1().Pods(ns).Create(context.Background(), pod, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("failed to create pod: %v", err) + } + t.Cleanup(func() { + _ = client.CoreV1().Pods(ns).Delete(context.Background(), created.Name, metav1.DeleteOptions{}) + }) + + opt := &domain.QueryDecisionMakerPodsOptions{ + K8SNamespace: []string{metav1.NamespaceAll}, + DecisionMakerLabel: domain.LabelSelector{ + Key: "dm", + Value: "true", + }, + } + + waitForLocal(t, func(ctx context.Context) (bool, error) { + opt.Result = opt.Result[:0] + if err := adapter.QueryDecisionMakerPods(ctx, opt); err != nil { + return false, err + } + if len(opt.Result) != 1 { + return false, nil + } + return opt.Result[0].NodeID != "" && opt.Result[0].Host != "", nil + }) + + { /*** Test Modifying DecisionMaker Pod ***/ + if err := retryUpdatePodLabel(context.Background(), client, ns, created.Name, "dm", "dm2"); err != nil { + t.Fatalf("failed to update pod: %v", err) + } + + opt.DecisionMakerLabel = domain.LabelSelector{Key: "dm", Value: "dm2"} + waitForLocal(t, func(ctx context.Context) (bool, error) { + opt.Result = opt.Result[:0] + if err := adapter.QueryDecisionMakerPods(ctx, opt); err != nil { + return false, err + } + if len(opt.Result) != 1 { + return false, nil + } + return opt.Result[0].Host != "" && opt.Result[0].NodeID != "" && opt.Result[0].Port == 8080, nil + }) + } + + { /*** Test Deleting DecisionMaker Pod ***/ + if err := client.CoreV1().Pods(ns).Delete(context.Background(), created.Name, metav1.DeleteOptions{}); err != nil { + t.Fatalf("failed to delete pod: %v", err) + } + + waitForLocal(t, func(ctx context.Context) (bool, error) { + opt.Result = opt.Result[:0] + if err := adapter.QueryDecisionMakerPods(ctx, opt); err != nil { + return false, err + } + return len(opt.Result) == 0, nil + }) + } + } +} diff --git a/manager/k8s_adapter/adapter_test.go b/manager/k8s_adapter/adapter_test.go new file mode 100644 index 0000000..9f0e2da --- /dev/null +++ b/manager/k8s_adapter/adapter_test.go @@ -0,0 +1,280 @@ +package k8sadapter + +import ( + "context" + "testing" + "time" + + "github.com/Gthulhu/api/manager/domain" + apiv1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes/fake" +) + +func TestPodWatcherCacheLifecycle(t *testing.T) { + t.Parallel() + + client := fake.NewSimpleClientset() + adapter := &Adapter{ + client: client, + podCache: make(map[string]apiv1.Pod), + stopCh: make(chan struct{}), + } + adapter.startPodWatcher() + t.Cleanup(adapter.StopPodWatcher) + + pod := &apiv1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-1", + Namespace: "ns", + UID: "uid-1", + Labels: map[string]string{ + "app": "demo", + }, + }, + Spec: apiv1.PodSpec{ + NodeName: "node-1", + Containers: []apiv1.Container{ + { + Name: "c1", + Command: []string{"run"}, + }, + }, + }, + Status: apiv1.PodStatus{ + Phase: apiv1.PodRunning, + ContainerStatuses: []apiv1.ContainerStatus{ + { + Name: "c1", + ContainerID: "docker://abc", + }, + }, + }, + } + + { /*** Test Creating Pod ***/ + if _, err := client.CoreV1().Pods("ns").Create(context.Background(), pod, metav1.CreateOptions{}); err != nil { + t.Fatalf("failed to create pod: %v", err) + } + + waitFor(t, func() bool { + adapter.podCacheMu.RLock() + defer adapter.podCacheMu.RUnlock() + return len(adapter.podCache) == 1 + }) + + opt := &domain.QueryPodsOptions{ + K8SNamespace: []string{"ns"}, + LabelSelectors: []domain.LabelSelector{ + {Key: "app", Value: "demo"}, + }, + } + + if err := adapter.QueryPods(context.Background(), opt); err != nil { + t.Fatalf("query pods after create: %v", err) + } + if len(opt.Result) != 1 { + t.Fatalf("expected 1 pod after create, got %d", len(opt.Result)) + } + if opt.Result[0].Containers[0].ContainerID != "docker://abc" { + t.Fatalf("unexpected container ID: %s", opt.Result[0].Containers[0].ContainerID) + } + } + + { /*** Test Modifying Pod ***/ + pod.Labels["app"] = "demo2" + if _, err := client.CoreV1().Pods("ns").Update(context.Background(), pod, metav1.UpdateOptions{}); err != nil { + t.Fatalf("failed to update pod: %v", err) + } + + waitFor(t, func() bool { + adapter.podCacheMu.RLock() + defer adapter.podCacheMu.RUnlock() + p, ok := adapter.podCache["uid-1"] + return ok && p.Labels["app"] == "demo2" + }) + + opt := &domain.QueryPodsOptions{ + K8SNamespace: []string{"ns"}, + LabelSelectors: []domain.LabelSelector{ + {Key: "app", Value: "demo2"}, + }, + } + if err := adapter.QueryPods(context.Background(), opt); err != nil { + t.Fatalf("query pods after update: %v", err) + } + if len(opt.Result) != 1 { + t.Fatalf("expected 1 pod after update, got %d", len(opt.Result)) + } + } + + { /*** Test Deleting Pod ***/ + if err := client.CoreV1().Pods("ns").Delete(context.Background(), pod.Name, metav1.DeleteOptions{}); err != nil { + t.Fatalf("failed to delete pod: %v", err) + } + + waitFor(t, func() bool { + adapter.podCacheMu.RLock() + defer adapter.podCacheMu.RUnlock() + _, ok := adapter.podCache["uid-1"] + return !ok + }) + + opt := &domain.QueryPodsOptions{ + K8SNamespace: []string{"ns"}, + LabelSelectors: []domain.LabelSelector{ + {Key: "app", Value: "demo2"}, + }, + } + if err := adapter.QueryPods(context.Background(), opt); err != nil { + t.Fatalf("query pods after delete: %v", err) + } + if len(opt.Result) != 0 { + t.Fatalf("expected no pod after delete, got %d", len(opt.Result)) + } + } +} + +func waitFor(t *testing.T, cond func() bool) { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + if err := wait.PollUntilContextTimeout(ctx, 10*time.Millisecond, 2*time.Second, false, func(context.Context) (done bool, err error) { + return cond(), nil + }); err != nil { + t.Fatalf("condition not met: %v", err) + } +} + +func TestQueryPodsUsesCache(t *testing.T) { + t.Parallel() + + adapter := &Adapter{ + client: fake.NewSimpleClientset(), + podCache: make(map[string]apiv1.Pod), + } + adapter.cacheHasSynced.Store(true) + + pod := apiv1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + UID: "uid-1", + Namespace: "ns1", + Labels: map[string]string{ + "app": "test", + }, + }, + Spec: apiv1.PodSpec{ + NodeName: "node-1", + Containers: []apiv1.Container{ + { + Name: "c1", + Command: []string{"cmd"}, + Args: []string{"--flag"}, + }, + }, + }, + Status: apiv1.PodStatus{ + ContainerStatuses: []apiv1.ContainerStatus{ + { + Name: "c1", + ContainerID: "docker://123", + }, + }, + }, + } + adapter.setPodCache(pod) + + opt := &domain.QueryPodsOptions{ + K8SNamespace: []string{"ns1"}, + LabelSelectors: []domain.LabelSelector{ + {Key: "app", Value: "test"}, + }, + } + + if err := adapter.QueryPods(context.Background(), opt); err != nil { + t.Fatalf("QueryPods returned error: %v", err) + } + + if len(opt.Result) != 1 { + t.Fatalf("expected 1 pod, got %d", len(opt.Result)) + } + + got := opt.Result[0] + if got.PodID != "uid-1" { + t.Fatalf("unexpected PodID %q", got.PodID) + } + if got.NodeID != "node-1" { + t.Fatalf("unexpected NodeID %q", got.NodeID) + } + if len(got.Containers) != 1 || got.Containers[0].ContainerID != "docker://123" { + t.Fatalf("unexpected container data %+v", got.Containers) + } +} + +func TestQueryDecisionMakerPodsUsesCache(t *testing.T) { + t.Parallel() + + adapter := &Adapter{ + client: fake.NewSimpleClientset(), + podCache: make(map[string]apiv1.Pod), + } + adapter.cacheHasSynced.Store(true) + + pod := apiv1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + UID: "uid-dm-1", + Namespace: "ns1", + Labels: map[string]string{ + "dm": "true", + }, + }, + Spec: apiv1.PodSpec{ + NodeName: "node-1", + Containers: []apiv1.Container{ + { + Name: "c1", + Ports: []apiv1.ContainerPort{{ContainerPort: 8080}}, + }, + }, + }, + Status: apiv1.PodStatus{ + Phase: apiv1.PodRunning, + PodIP: "10.0.0.1", + HostIP: "10.0.0.2", + }, + } + adapter.setPodCache(pod) + + opt := &domain.QueryDecisionMakerPodsOptions{ + K8SNamespace: []string{"ns1"}, + NodeIDs: []string{"node-1"}, + DecisionMakerLabel: domain.LabelSelector{ + Key: "dm", + Value: "true", + }, + } + + if err := adapter.QueryDecisionMakerPods(context.Background(), opt); err != nil { + t.Fatalf("QueryDecisionMakerPods returned error: %v", err) + } + + if len(opt.Result) != 1 { + t.Fatalf("expected 1 decision maker pod, got %d", len(opt.Result)) + } + + got := opt.Result[0] + if got.NodeID != "node-1" { + t.Fatalf("unexpected NodeID %q", got.NodeID) + } + if got.Port != 8080 { + t.Fatalf("unexpected port %d", got.Port) + } + if got.Host != "10.0.0.1" { + t.Fatalf("unexpected host %q", got.Host) + } + if got.State != domain.NodeStateOnline { + t.Fatalf("unexpected state %v", got.State) + } +} diff --git a/manager/k8s_adapter/init b/manager/k8s_adapter/init deleted file mode 100644 index e69de29..0000000 From d586c4abb80b724ef968016890926901c7d84514 Mon Sep 17 00:00:00 2001 From: Vic Xu Date: Wed, 10 Dec 2025 20:20:56 +0800 Subject: [PATCH 15/18] strategy/api: implement strategy post and get api --- .mockery.yaml | 2 +- config/manager_config.default.toml | 4 + config/manager_config.go | 6 + config/manager_config.test.toml | 5 + go.mod | 1 + manager/app/module.go | 21 + manager/client/.keep | 0 manager/client/deicison_maker.go | 22 + manager/domain/enums.go | 21 +- manager/domain/interface.go | 36 +- manager/domain/k8s_resource.go | 11 + manager/domain/mock_domain.go | 2104 +++++++++++++++++ manager/domain/strategy.go | 46 +- manager/k8s_adapter/adapter.go | 35 +- manager/k8s_adapter/adapter_local_test.go | 44 +- manager/k8s_adapter/adapter_test.go | 44 +- .../migration/001_init_collections.down.json | 4 +- .../migration/001_init_collections.up.json | 118 + .../002_init_default_permissions.up.json | 18 + .../migration/003_init_default_roles.up.json | 5 +- manager/repository/repo.go | 276 +-- manager/repository/repo_rbac.go | 271 +++ manager/repository/strategy_repo.go | 128 + manager/rest/handler_test.go | 17 + manager/rest/routes.go | 5 + manager/rest/strategy_hdl.go | 197 ++ manager/rest/strategy_hdl_test.go | 65 + manager/service/strategy_svc.go | 107 + manager/service/svc.go | 6 + 29 files changed, 3260 insertions(+), 359 deletions(-) delete mode 100644 manager/client/.keep create mode 100644 manager/client/deicison_maker.go create mode 100644 manager/domain/mock_domain.go create mode 100644 manager/repository/repo_rbac.go create mode 100644 manager/repository/strategy_repo.go create mode 100644 manager/rest/strategy_hdl.go create mode 100644 manager/rest/strategy_hdl_test.go create mode 100644 manager/service/strategy_svc.go diff --git a/.mockery.yaml b/.mockery.yaml index 81a6578..4289eb1 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -12,6 +12,6 @@ template: testify template-schema: "{{.Template}}.schema.json" packages: - github.com/Gthulhu/api/adapter/kubernetes: + github.com/Gthulhu/api/manager/domain: config: all: true \ No newline at end of file diff --git a/config/manager_config.default.toml b/config/manager_config.default.toml index aef2c72..5e8659a 100644 --- a/config/manager_config.default.toml +++ b/config/manager_config.default.toml @@ -34,3 +34,7 @@ YOUR_PUBLIC_KEY_HERE [account] admin_email = "admin@example.com" admin_password = "your-password-here" + +[k8s] +kube_config_path = "/path/to/kubeconfig" +in_cluster = false \ No newline at end of file diff --git a/config/manager_config.go b/config/manager_config.go index 7874ee9..8b0120a 100644 --- a/config/manager_config.go +++ b/config/manager_config.go @@ -35,6 +35,7 @@ type ManageConfig struct { MongoDB MongoDBConfig `mapstructure:"mongodb"` Key KeyConfig `mapstructure:"key"` Account AccountConfig `mapstructure:"account"` + K8S K8SConfig `mapstructure:"k8s"` } type MongoDBConfig struct { @@ -60,6 +61,11 @@ type AccountConfig struct { AdminPassword SecretValue `mapstructure:"admin_password"` } +type K8SConfig struct { + KubeConfigPath string `mapstructure:"kube_config_path"` + IsInCluster bool `mapstructure:"in_cluster"` +} + var ( managerCfg *ManageConfig ) diff --git a/config/manager_config.test.toml b/config/manager_config.test.toml index 210d170..5cedb7a 100644 --- a/config/manager_config.test.toml +++ b/config/manager_config.test.toml @@ -73,3 +73,8 @@ X6m7Mp9nAMhRyXhULslO3trWFbFCa2dbQkDSyBRvsb2HZtztoLVyo1mtUg== [account] admin_email = "admin@example.com" admin_password = "your-password-here" + + +[k8s] +kube_config_path = "/path/to/kubeconfig" +in_cluster = false \ No newline at end of file diff --git a/go.mod b/go.mod index 755f3c9..c13ebcc 100644 --- a/go.mod +++ b/go.mod @@ -102,6 +102,7 @@ require ( github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect diff --git a/manager/app/module.go b/manager/app/module.go index c8fb637..c2371cb 100644 --- a/manager/app/module.go +++ b/manager/app/module.go @@ -2,6 +2,9 @@ package app import ( "github.com/Gthulhu/api/config" + "github.com/Gthulhu/api/manager/client" + "github.com/Gthulhu/api/manager/domain" + k8sadapter "github.com/Gthulhu/api/manager/k8s_adapter" "github.com/Gthulhu/api/manager/repository" "github.com/Gthulhu/api/manager/rest" "github.com/Gthulhu/api/manager/service" @@ -27,6 +30,24 @@ func ConfigModule(cfg config.ManageConfig) (fx.Option, error) { fx.Provide(func(managerCfg config.ManageConfig) config.AccountConfig { return managerCfg.Account }), + fx.Provide(func(managerCfg config.ManageConfig) config.K8SConfig { + return managerCfg.K8S + }), + ), nil +} + +// AdapterModule creates an Fx module that provides the K8S adapter and Decision Maker client +func AdapterModule() (fx.Option, error) { + return fx.Options( + fx.Provide(func(k8sConfig config.K8SConfig) (domain.K8SAdapter, error) { + return k8sadapter.NewAdapter(k8sadapter.Options{ + KubeConfigPath: k8sConfig.KubeConfigPath, + InCluster: k8sConfig.IsInCluster, + }) + }), + fx.Provide(func() domain.DecisionMakerAdapter { + return client.NewDecisionMakerClient() + }), ), nil } diff --git a/manager/client/.keep b/manager/client/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/manager/client/deicison_maker.go b/manager/client/deicison_maker.go new file mode 100644 index 0000000..b243e7b --- /dev/null +++ b/manager/client/deicison_maker.go @@ -0,0 +1,22 @@ +package client + +import ( + "context" + "errors" + "net/http" + + "github.com/Gthulhu/api/manager/domain" +) + +func NewDecisionMakerClient() domain.DecisionMakerAdapter { + return &DecisionMakerClient{} +} + +type DecisionMakerClient struct { + http.Client +} + +func (dm DecisionMakerClient) SendSchedulingIntent(ctx context.Context, decisionMaker *domain.DecisionMakerPod, intents []*domain.ScheduleIntent) error { + // TODO: Implementation of sending scheduling intents to the decision maker pod + return errors.New("not implemented") +} diff --git a/manager/domain/enums.go b/manager/domain/enums.go index f5cba61..be957be 100644 --- a/manager/domain/enums.go +++ b/manager/domain/enums.go @@ -3,15 +3,18 @@ package domain type PermissionKey string const ( - CreateUser PermissionKey = "user.create" - UserRead PermissionKey = "user.read" - ChangeUserPermission PermissionKey = "user.permission.update" - ResetUserPassword PermissionKey = "user.password.reset" - RoleCrete PermissionKey = "role.create" - RoleRead PermissionKey = "role.read" - RoleUpdate PermissionKey = "role.update" - RoleDelete PermissionKey = "role.delete" - PermissionRead PermissionKey = "permission.read" + CreateUser PermissionKey = "user.create" + UserRead PermissionKey = "user.read" + ChangeUserPermission PermissionKey = "user.permission.update" + ResetUserPassword PermissionKey = "user.password.reset" + RoleCrete PermissionKey = "role.create" + RoleRead PermissionKey = "role.read" + RoleUpdate PermissionKey = "role.update" + RoleDelete PermissionKey = "role.delete" + PermissionRead PermissionKey = "permission.read" + ScheduleStrategyCreate PermissionKey = "schedule_strategy.create" + ScheduleStrategyRead PermissionKey = "schedule_strategy.read" + ScheduleIntentRead PermissionKey = "schedule_intent.read" ) const ( diff --git a/manager/domain/interface.go b/manager/domain/interface.go index 8d0ca58..57dac91 100644 --- a/manager/domain/interface.go +++ b/manager/domain/interface.go @@ -32,6 +32,23 @@ type QueryAuditLogOptions struct { Result []*AuditLog } +type QueryStrategyOptions struct { + IDs []bson.ObjectID + K8SNamespaces []string + Result []*ScheduleStrategy + CreatorIDs []bson.ObjectID +} + +type QueryIntentOptions struct { + IDs []bson.ObjectID + K8SNamespaces []string + StrategyIDs []bson.ObjectID + States []IntentState + PodIDs []string + Result []*ScheduleIntent + CreatorIDs []bson.ObjectID +} + type Repository interface { CreateUser(ctx context.Context, user *User) error UpdateUser(ctx context.Context, user *User) error @@ -44,6 +61,11 @@ type Repository interface { QueryPermissions(ctx context.Context, opt *QueryPermissionOptions) error CreateAuditLog(ctx context.Context, log *AuditLog) error QueryAuditLogs(ctx context.Context, opt *QueryAuditLogOptions) error + + InsertStrategyAndIntents(ctx context.Context, strategy *ScheduleStrategy, intents []*ScheduleIntent) error + BatchUpdateIntentsState(ctx context.Context, intentIDs []bson.ObjectID, newState IntentState) error + QueryStrategies(ctx context.Context, opt *QueryStrategyOptions) error + QueryIntents(ctx context.Context, opt *QueryIntentOptions) error } type Service interface { @@ -62,24 +84,28 @@ type Service interface { QueryRoles(ctx context.Context, opt *QueryRoleOptions) error QueryPermissions(ctx context.Context, opt *QueryPermissionOptions) error - ListAuditLogs(ctx context.Context, opt *QueryAuditLogOptions) error + CreateScheduleStrategy(ctx context.Context, operator *Claims, strategy *ScheduleStrategy) error + ListScheduleStrategies(ctx context.Context, filterOpts *QueryStrategyOptions) error + ListScheduleIntents(ctx context.Context, filterOpts *QueryIntentOptions) error } type QueryPodsOptions struct { K8SNamespace []string LabelSelectors []LabelSelector CommandRegex string - Result []*Pod } type QueryDecisionMakerPodsOptions struct { K8SNamespace []string NodeIDs []string DecisionMakerLabel LabelSelector - Result []*DecisionMakerPod } type K8SAdapter interface { - QueryPods(ctx context.Context, opt *QueryPodsOptions) error - QueryDecisionMakerPods(ctx context.Context, opt *QueryDecisionMakerPodsOptions) error + QueryPods(ctx context.Context, opt *QueryPodsOptions) ([]*Pod, error) + QueryDecisionMakerPods(ctx context.Context, opt *QueryDecisionMakerPodsOptions) ([]*DecisionMakerPod, error) +} + +type DecisionMakerAdapter interface { + SendSchedulingIntent(ctx context.Context, decisionMaker *DecisionMakerPod, intents []*ScheduleIntent) error } diff --git a/manager/domain/k8s_resource.go b/manager/domain/k8s_resource.go index 1c3c6f8..1951ca6 100644 --- a/manager/domain/k8s_resource.go +++ b/manager/domain/k8s_resource.go @@ -15,6 +15,17 @@ type Pod struct { Containers []Container } +func (p *Pod) LabelsToSelectors() []LabelSelector { + selectors := make([]LabelSelector, 0, len(p.Labels)) + for k, v := range p.Labels { + selectors = append(selectors, LabelSelector{ + Key: k, + Value: v, + }) + } + return selectors +} + type Container struct { ContainerID string Name string diff --git a/manager/domain/mock_domain.go b/manager/domain/mock_domain.go new file mode 100644 index 0000000..2960046 --- /dev/null +++ b/manager/domain/mock_domain.go @@ -0,0 +1,2104 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package domain + +import ( + "context" + + mock "github.com/stretchr/testify/mock" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// NewMockRepository creates a new instance of MockRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockRepository(t interface { + mock.TestingT + Cleanup(func()) +}) *MockRepository { + mock := &MockRepository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockRepository is an autogenerated mock type for the Repository type +type MockRepository struct { + mock.Mock +} + +type MockRepository_Expecter struct { + mock *mock.Mock +} + +func (_m *MockRepository) EXPECT() *MockRepository_Expecter { + return &MockRepository_Expecter{mock: &_m.Mock} +} + +// BatchUpdateIntentsState provides a mock function for the type MockRepository +func (_mock *MockRepository) BatchUpdateIntentsState(ctx context.Context, intentIDs []bson.ObjectID, newState IntentState) error { + ret := _mock.Called(ctx, intentIDs, newState) + + if len(ret) == 0 { + panic("no return value specified for BatchUpdateIntentsState") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, []bson.ObjectID, IntentState) error); ok { + r0 = returnFunc(ctx, intentIDs, newState) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockRepository_BatchUpdateIntentsState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BatchUpdateIntentsState' +type MockRepository_BatchUpdateIntentsState_Call struct { + *mock.Call +} + +// BatchUpdateIntentsState is a helper method to define mock.On call +// - ctx context.Context +// - intentIDs []bson.ObjectID +// - newState IntentState +func (_e *MockRepository_Expecter) BatchUpdateIntentsState(ctx interface{}, intentIDs interface{}, newState interface{}) *MockRepository_BatchUpdateIntentsState_Call { + return &MockRepository_BatchUpdateIntentsState_Call{Call: _e.mock.On("BatchUpdateIntentsState", ctx, intentIDs, newState)} +} + +func (_c *MockRepository_BatchUpdateIntentsState_Call) Run(run func(ctx context.Context, intentIDs []bson.ObjectID, newState IntentState)) *MockRepository_BatchUpdateIntentsState_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []bson.ObjectID + if args[1] != nil { + arg1 = args[1].([]bson.ObjectID) + } + var arg2 IntentState + if args[2] != nil { + arg2 = args[2].(IntentState) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockRepository_BatchUpdateIntentsState_Call) Return(err error) *MockRepository_BatchUpdateIntentsState_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockRepository_BatchUpdateIntentsState_Call) RunAndReturn(run func(ctx context.Context, intentIDs []bson.ObjectID, newState IntentState) error) *MockRepository_BatchUpdateIntentsState_Call { + _c.Call.Return(run) + return _c +} + +// CreateAuditLog provides a mock function for the type MockRepository +func (_mock *MockRepository) CreateAuditLog(ctx context.Context, log *AuditLog) error { + ret := _mock.Called(ctx, log) + + if len(ret) == 0 { + panic("no return value specified for CreateAuditLog") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *AuditLog) error); ok { + r0 = returnFunc(ctx, log) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockRepository_CreateAuditLog_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateAuditLog' +type MockRepository_CreateAuditLog_Call struct { + *mock.Call +} + +// CreateAuditLog is a helper method to define mock.On call +// - ctx context.Context +// - log *AuditLog +func (_e *MockRepository_Expecter) CreateAuditLog(ctx interface{}, log interface{}) *MockRepository_CreateAuditLog_Call { + return &MockRepository_CreateAuditLog_Call{Call: _e.mock.On("CreateAuditLog", ctx, log)} +} + +func (_c *MockRepository_CreateAuditLog_Call) Run(run func(ctx context.Context, log *AuditLog)) *MockRepository_CreateAuditLog_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *AuditLog + if args[1] != nil { + arg1 = args[1].(*AuditLog) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRepository_CreateAuditLog_Call) Return(err error) *MockRepository_CreateAuditLog_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockRepository_CreateAuditLog_Call) RunAndReturn(run func(ctx context.Context, log *AuditLog) error) *MockRepository_CreateAuditLog_Call { + _c.Call.Return(run) + return _c +} + +// CreatePermission provides a mock function for the type MockRepository +func (_mock *MockRepository) CreatePermission(ctx context.Context, permission *Permission) error { + ret := _mock.Called(ctx, permission) + + if len(ret) == 0 { + panic("no return value specified for CreatePermission") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *Permission) error); ok { + r0 = returnFunc(ctx, permission) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockRepository_CreatePermission_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreatePermission' +type MockRepository_CreatePermission_Call struct { + *mock.Call +} + +// CreatePermission is a helper method to define mock.On call +// - ctx context.Context +// - permission *Permission +func (_e *MockRepository_Expecter) CreatePermission(ctx interface{}, permission interface{}) *MockRepository_CreatePermission_Call { + return &MockRepository_CreatePermission_Call{Call: _e.mock.On("CreatePermission", ctx, permission)} +} + +func (_c *MockRepository_CreatePermission_Call) Run(run func(ctx context.Context, permission *Permission)) *MockRepository_CreatePermission_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *Permission + if args[1] != nil { + arg1 = args[1].(*Permission) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRepository_CreatePermission_Call) Return(err error) *MockRepository_CreatePermission_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockRepository_CreatePermission_Call) RunAndReturn(run func(ctx context.Context, permission *Permission) error) *MockRepository_CreatePermission_Call { + _c.Call.Return(run) + return _c +} + +// CreateRole provides a mock function for the type MockRepository +func (_mock *MockRepository) CreateRole(ctx context.Context, role *Role) error { + ret := _mock.Called(ctx, role) + + if len(ret) == 0 { + panic("no return value specified for CreateRole") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *Role) error); ok { + r0 = returnFunc(ctx, role) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockRepository_CreateRole_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateRole' +type MockRepository_CreateRole_Call struct { + *mock.Call +} + +// CreateRole is a helper method to define mock.On call +// - ctx context.Context +// - role *Role +func (_e *MockRepository_Expecter) CreateRole(ctx interface{}, role interface{}) *MockRepository_CreateRole_Call { + return &MockRepository_CreateRole_Call{Call: _e.mock.On("CreateRole", ctx, role)} +} + +func (_c *MockRepository_CreateRole_Call) Run(run func(ctx context.Context, role *Role)) *MockRepository_CreateRole_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *Role + if args[1] != nil { + arg1 = args[1].(*Role) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRepository_CreateRole_Call) Return(err error) *MockRepository_CreateRole_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockRepository_CreateRole_Call) RunAndReturn(run func(ctx context.Context, role *Role) error) *MockRepository_CreateRole_Call { + _c.Call.Return(run) + return _c +} + +// CreateUser provides a mock function for the type MockRepository +func (_mock *MockRepository) CreateUser(ctx context.Context, user *User) error { + ret := _mock.Called(ctx, user) + + if len(ret) == 0 { + panic("no return value specified for CreateUser") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *User) error); ok { + r0 = returnFunc(ctx, user) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockRepository_CreateUser_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateUser' +type MockRepository_CreateUser_Call struct { + *mock.Call +} + +// CreateUser is a helper method to define mock.On call +// - ctx context.Context +// - user *User +func (_e *MockRepository_Expecter) CreateUser(ctx interface{}, user interface{}) *MockRepository_CreateUser_Call { + return &MockRepository_CreateUser_Call{Call: _e.mock.On("CreateUser", ctx, user)} +} + +func (_c *MockRepository_CreateUser_Call) Run(run func(ctx context.Context, user *User)) *MockRepository_CreateUser_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *User + if args[1] != nil { + arg1 = args[1].(*User) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRepository_CreateUser_Call) Return(err error) *MockRepository_CreateUser_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockRepository_CreateUser_Call) RunAndReturn(run func(ctx context.Context, user *User) error) *MockRepository_CreateUser_Call { + _c.Call.Return(run) + return _c +} + +// InsertStrategyAndIntents provides a mock function for the type MockRepository +func (_mock *MockRepository) InsertStrategyAndIntents(ctx context.Context, strategy *ScheduleStrategy, intents []*ScheduleIntent) error { + ret := _mock.Called(ctx, strategy, intents) + + if len(ret) == 0 { + panic("no return value specified for InsertStrategyAndIntents") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *ScheduleStrategy, []*ScheduleIntent) error); ok { + r0 = returnFunc(ctx, strategy, intents) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockRepository_InsertStrategyAndIntents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InsertStrategyAndIntents' +type MockRepository_InsertStrategyAndIntents_Call struct { + *mock.Call +} + +// InsertStrategyAndIntents is a helper method to define mock.On call +// - ctx context.Context +// - strategy *ScheduleStrategy +// - intents []*ScheduleIntent +func (_e *MockRepository_Expecter) InsertStrategyAndIntents(ctx interface{}, strategy interface{}, intents interface{}) *MockRepository_InsertStrategyAndIntents_Call { + return &MockRepository_InsertStrategyAndIntents_Call{Call: _e.mock.On("InsertStrategyAndIntents", ctx, strategy, intents)} +} + +func (_c *MockRepository_InsertStrategyAndIntents_Call) Run(run func(ctx context.Context, strategy *ScheduleStrategy, intents []*ScheduleIntent)) *MockRepository_InsertStrategyAndIntents_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *ScheduleStrategy + if args[1] != nil { + arg1 = args[1].(*ScheduleStrategy) + } + var arg2 []*ScheduleIntent + if args[2] != nil { + arg2 = args[2].([]*ScheduleIntent) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockRepository_InsertStrategyAndIntents_Call) Return(err error) *MockRepository_InsertStrategyAndIntents_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockRepository_InsertStrategyAndIntents_Call) RunAndReturn(run func(ctx context.Context, strategy *ScheduleStrategy, intents []*ScheduleIntent) error) *MockRepository_InsertStrategyAndIntents_Call { + _c.Call.Return(run) + return _c +} + +// QueryAuditLogs provides a mock function for the type MockRepository +func (_mock *MockRepository) QueryAuditLogs(ctx context.Context, opt *QueryAuditLogOptions) error { + ret := _mock.Called(ctx, opt) + + if len(ret) == 0 { + panic("no return value specified for QueryAuditLogs") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *QueryAuditLogOptions) error); ok { + r0 = returnFunc(ctx, opt) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockRepository_QueryAuditLogs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QueryAuditLogs' +type MockRepository_QueryAuditLogs_Call struct { + *mock.Call +} + +// QueryAuditLogs is a helper method to define mock.On call +// - ctx context.Context +// - opt *QueryAuditLogOptions +func (_e *MockRepository_Expecter) QueryAuditLogs(ctx interface{}, opt interface{}) *MockRepository_QueryAuditLogs_Call { + return &MockRepository_QueryAuditLogs_Call{Call: _e.mock.On("QueryAuditLogs", ctx, opt)} +} + +func (_c *MockRepository_QueryAuditLogs_Call) Run(run func(ctx context.Context, opt *QueryAuditLogOptions)) *MockRepository_QueryAuditLogs_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *QueryAuditLogOptions + if args[1] != nil { + arg1 = args[1].(*QueryAuditLogOptions) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRepository_QueryAuditLogs_Call) Return(err error) *MockRepository_QueryAuditLogs_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockRepository_QueryAuditLogs_Call) RunAndReturn(run func(ctx context.Context, opt *QueryAuditLogOptions) error) *MockRepository_QueryAuditLogs_Call { + _c.Call.Return(run) + return _c +} + +// QueryIntents provides a mock function for the type MockRepository +func (_mock *MockRepository) QueryIntents(ctx context.Context, opt *QueryIntentOptions) error { + ret := _mock.Called(ctx, opt) + + if len(ret) == 0 { + panic("no return value specified for QueryIntents") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *QueryIntentOptions) error); ok { + r0 = returnFunc(ctx, opt) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockRepository_QueryIntents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QueryIntents' +type MockRepository_QueryIntents_Call struct { + *mock.Call +} + +// QueryIntents is a helper method to define mock.On call +// - ctx context.Context +// - opt *QueryIntentOptions +func (_e *MockRepository_Expecter) QueryIntents(ctx interface{}, opt interface{}) *MockRepository_QueryIntents_Call { + return &MockRepository_QueryIntents_Call{Call: _e.mock.On("QueryIntents", ctx, opt)} +} + +func (_c *MockRepository_QueryIntents_Call) Run(run func(ctx context.Context, opt *QueryIntentOptions)) *MockRepository_QueryIntents_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *QueryIntentOptions + if args[1] != nil { + arg1 = args[1].(*QueryIntentOptions) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRepository_QueryIntents_Call) Return(err error) *MockRepository_QueryIntents_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockRepository_QueryIntents_Call) RunAndReturn(run func(ctx context.Context, opt *QueryIntentOptions) error) *MockRepository_QueryIntents_Call { + _c.Call.Return(run) + return _c +} + +// QueryPermissions provides a mock function for the type MockRepository +func (_mock *MockRepository) QueryPermissions(ctx context.Context, opt *QueryPermissionOptions) error { + ret := _mock.Called(ctx, opt) + + if len(ret) == 0 { + panic("no return value specified for QueryPermissions") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *QueryPermissionOptions) error); ok { + r0 = returnFunc(ctx, opt) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockRepository_QueryPermissions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QueryPermissions' +type MockRepository_QueryPermissions_Call struct { + *mock.Call +} + +// QueryPermissions is a helper method to define mock.On call +// - ctx context.Context +// - opt *QueryPermissionOptions +func (_e *MockRepository_Expecter) QueryPermissions(ctx interface{}, opt interface{}) *MockRepository_QueryPermissions_Call { + return &MockRepository_QueryPermissions_Call{Call: _e.mock.On("QueryPermissions", ctx, opt)} +} + +func (_c *MockRepository_QueryPermissions_Call) Run(run func(ctx context.Context, opt *QueryPermissionOptions)) *MockRepository_QueryPermissions_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *QueryPermissionOptions + if args[1] != nil { + arg1 = args[1].(*QueryPermissionOptions) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRepository_QueryPermissions_Call) Return(err error) *MockRepository_QueryPermissions_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockRepository_QueryPermissions_Call) RunAndReturn(run func(ctx context.Context, opt *QueryPermissionOptions) error) *MockRepository_QueryPermissions_Call { + _c.Call.Return(run) + return _c +} + +// QueryRoles provides a mock function for the type MockRepository +func (_mock *MockRepository) QueryRoles(ctx context.Context, opt *QueryRoleOptions) error { + ret := _mock.Called(ctx, opt) + + if len(ret) == 0 { + panic("no return value specified for QueryRoles") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *QueryRoleOptions) error); ok { + r0 = returnFunc(ctx, opt) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockRepository_QueryRoles_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QueryRoles' +type MockRepository_QueryRoles_Call struct { + *mock.Call +} + +// QueryRoles is a helper method to define mock.On call +// - ctx context.Context +// - opt *QueryRoleOptions +func (_e *MockRepository_Expecter) QueryRoles(ctx interface{}, opt interface{}) *MockRepository_QueryRoles_Call { + return &MockRepository_QueryRoles_Call{Call: _e.mock.On("QueryRoles", ctx, opt)} +} + +func (_c *MockRepository_QueryRoles_Call) Run(run func(ctx context.Context, opt *QueryRoleOptions)) *MockRepository_QueryRoles_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *QueryRoleOptions + if args[1] != nil { + arg1 = args[1].(*QueryRoleOptions) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRepository_QueryRoles_Call) Return(err error) *MockRepository_QueryRoles_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockRepository_QueryRoles_Call) RunAndReturn(run func(ctx context.Context, opt *QueryRoleOptions) error) *MockRepository_QueryRoles_Call { + _c.Call.Return(run) + return _c +} + +// QueryStrategies provides a mock function for the type MockRepository +func (_mock *MockRepository) QueryStrategies(ctx context.Context, opt *QueryStrategyOptions) error { + ret := _mock.Called(ctx, opt) + + if len(ret) == 0 { + panic("no return value specified for QueryStrategies") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *QueryStrategyOptions) error); ok { + r0 = returnFunc(ctx, opt) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockRepository_QueryStrategies_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QueryStrategies' +type MockRepository_QueryStrategies_Call struct { + *mock.Call +} + +// QueryStrategies is a helper method to define mock.On call +// - ctx context.Context +// - opt *QueryStrategyOptions +func (_e *MockRepository_Expecter) QueryStrategies(ctx interface{}, opt interface{}) *MockRepository_QueryStrategies_Call { + return &MockRepository_QueryStrategies_Call{Call: _e.mock.On("QueryStrategies", ctx, opt)} +} + +func (_c *MockRepository_QueryStrategies_Call) Run(run func(ctx context.Context, opt *QueryStrategyOptions)) *MockRepository_QueryStrategies_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *QueryStrategyOptions + if args[1] != nil { + arg1 = args[1].(*QueryStrategyOptions) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRepository_QueryStrategies_Call) Return(err error) *MockRepository_QueryStrategies_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockRepository_QueryStrategies_Call) RunAndReturn(run func(ctx context.Context, opt *QueryStrategyOptions) error) *MockRepository_QueryStrategies_Call { + _c.Call.Return(run) + return _c +} + +// QueryUsers provides a mock function for the type MockRepository +func (_mock *MockRepository) QueryUsers(ctx context.Context, opt *QueryUserOptions) error { + ret := _mock.Called(ctx, opt) + + if len(ret) == 0 { + panic("no return value specified for QueryUsers") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *QueryUserOptions) error); ok { + r0 = returnFunc(ctx, opt) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockRepository_QueryUsers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QueryUsers' +type MockRepository_QueryUsers_Call struct { + *mock.Call +} + +// QueryUsers is a helper method to define mock.On call +// - ctx context.Context +// - opt *QueryUserOptions +func (_e *MockRepository_Expecter) QueryUsers(ctx interface{}, opt interface{}) *MockRepository_QueryUsers_Call { + return &MockRepository_QueryUsers_Call{Call: _e.mock.On("QueryUsers", ctx, opt)} +} + +func (_c *MockRepository_QueryUsers_Call) Run(run func(ctx context.Context, opt *QueryUserOptions)) *MockRepository_QueryUsers_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *QueryUserOptions + if args[1] != nil { + arg1 = args[1].(*QueryUserOptions) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRepository_QueryUsers_Call) Return(err error) *MockRepository_QueryUsers_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockRepository_QueryUsers_Call) RunAndReturn(run func(ctx context.Context, opt *QueryUserOptions) error) *MockRepository_QueryUsers_Call { + _c.Call.Return(run) + return _c +} + +// UpdatePermission provides a mock function for the type MockRepository +func (_mock *MockRepository) UpdatePermission(ctx context.Context, permission *Permission) error { + ret := _mock.Called(ctx, permission) + + if len(ret) == 0 { + panic("no return value specified for UpdatePermission") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *Permission) error); ok { + r0 = returnFunc(ctx, permission) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockRepository_UpdatePermission_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdatePermission' +type MockRepository_UpdatePermission_Call struct { + *mock.Call +} + +// UpdatePermission is a helper method to define mock.On call +// - ctx context.Context +// - permission *Permission +func (_e *MockRepository_Expecter) UpdatePermission(ctx interface{}, permission interface{}) *MockRepository_UpdatePermission_Call { + return &MockRepository_UpdatePermission_Call{Call: _e.mock.On("UpdatePermission", ctx, permission)} +} + +func (_c *MockRepository_UpdatePermission_Call) Run(run func(ctx context.Context, permission *Permission)) *MockRepository_UpdatePermission_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *Permission + if args[1] != nil { + arg1 = args[1].(*Permission) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRepository_UpdatePermission_Call) Return(err error) *MockRepository_UpdatePermission_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockRepository_UpdatePermission_Call) RunAndReturn(run func(ctx context.Context, permission *Permission) error) *MockRepository_UpdatePermission_Call { + _c.Call.Return(run) + return _c +} + +// UpdateRole provides a mock function for the type MockRepository +func (_mock *MockRepository) UpdateRole(ctx context.Context, role *Role) error { + ret := _mock.Called(ctx, role) + + if len(ret) == 0 { + panic("no return value specified for UpdateRole") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *Role) error); ok { + r0 = returnFunc(ctx, role) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockRepository_UpdateRole_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateRole' +type MockRepository_UpdateRole_Call struct { + *mock.Call +} + +// UpdateRole is a helper method to define mock.On call +// - ctx context.Context +// - role *Role +func (_e *MockRepository_Expecter) UpdateRole(ctx interface{}, role interface{}) *MockRepository_UpdateRole_Call { + return &MockRepository_UpdateRole_Call{Call: _e.mock.On("UpdateRole", ctx, role)} +} + +func (_c *MockRepository_UpdateRole_Call) Run(run func(ctx context.Context, role *Role)) *MockRepository_UpdateRole_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *Role + if args[1] != nil { + arg1 = args[1].(*Role) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRepository_UpdateRole_Call) Return(err error) *MockRepository_UpdateRole_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockRepository_UpdateRole_Call) RunAndReturn(run func(ctx context.Context, role *Role) error) *MockRepository_UpdateRole_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUser provides a mock function for the type MockRepository +func (_mock *MockRepository) UpdateUser(ctx context.Context, user *User) error { + ret := _mock.Called(ctx, user) + + if len(ret) == 0 { + panic("no return value specified for UpdateUser") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *User) error); ok { + r0 = returnFunc(ctx, user) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockRepository_UpdateUser_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUser' +type MockRepository_UpdateUser_Call struct { + *mock.Call +} + +// UpdateUser is a helper method to define mock.On call +// - ctx context.Context +// - user *User +func (_e *MockRepository_Expecter) UpdateUser(ctx interface{}, user interface{}) *MockRepository_UpdateUser_Call { + return &MockRepository_UpdateUser_Call{Call: _e.mock.On("UpdateUser", ctx, user)} +} + +func (_c *MockRepository_UpdateUser_Call) Run(run func(ctx context.Context, user *User)) *MockRepository_UpdateUser_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *User + if args[1] != nil { + arg1 = args[1].(*User) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRepository_UpdateUser_Call) Return(err error) *MockRepository_UpdateUser_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockRepository_UpdateUser_Call) RunAndReturn(run func(ctx context.Context, user *User) error) *MockRepository_UpdateUser_Call { + _c.Call.Return(run) + return _c +} + +// NewMockService creates a new instance of MockService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockService(t interface { + mock.TestingT + Cleanup(func()) +}) *MockService { + mock := &MockService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockService is an autogenerated mock type for the Service type +type MockService struct { + mock.Mock +} + +type MockService_Expecter struct { + mock *mock.Mock +} + +func (_m *MockService) EXPECT() *MockService_Expecter { + return &MockService_Expecter{mock: &_m.Mock} +} + +// ChangePassword provides a mock function for the type MockService +func (_mock *MockService) ChangePassword(ctx context.Context, user *Claims, oldPassword string, newPassword string) error { + ret := _mock.Called(ctx, user, oldPassword, newPassword) + + if len(ret) == 0 { + panic("no return value specified for ChangePassword") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *Claims, string, string) error); ok { + r0 = returnFunc(ctx, user, oldPassword, newPassword) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockService_ChangePassword_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChangePassword' +type MockService_ChangePassword_Call struct { + *mock.Call +} + +// ChangePassword is a helper method to define mock.On call +// - ctx context.Context +// - user *Claims +// - oldPassword string +// - newPassword string +func (_e *MockService_Expecter) ChangePassword(ctx interface{}, user interface{}, oldPassword interface{}, newPassword interface{}) *MockService_ChangePassword_Call { + return &MockService_ChangePassword_Call{Call: _e.mock.On("ChangePassword", ctx, user, oldPassword, newPassword)} +} + +func (_c *MockService_ChangePassword_Call) Run(run func(ctx context.Context, user *Claims, oldPassword string, newPassword string)) *MockService_ChangePassword_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *Claims + if args[1] != nil { + arg1 = args[1].(*Claims) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 string + if args[3] != nil { + arg3 = args[3].(string) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *MockService_ChangePassword_Call) Return(err error) *MockService_ChangePassword_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockService_ChangePassword_Call) RunAndReturn(run func(ctx context.Context, user *Claims, oldPassword string, newPassword string) error) *MockService_ChangePassword_Call { + _c.Call.Return(run) + return _c +} + +// CreateAdminUserIfNotExists provides a mock function for the type MockService +func (_mock *MockService) CreateAdminUserIfNotExists(ctx context.Context, username string, password string) error { + ret := _mock.Called(ctx, username, password) + + if len(ret) == 0 { + panic("no return value specified for CreateAdminUserIfNotExists") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = returnFunc(ctx, username, password) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockService_CreateAdminUserIfNotExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateAdminUserIfNotExists' +type MockService_CreateAdminUserIfNotExists_Call struct { + *mock.Call +} + +// CreateAdminUserIfNotExists is a helper method to define mock.On call +// - ctx context.Context +// - username string +// - password string +func (_e *MockService_Expecter) CreateAdminUserIfNotExists(ctx interface{}, username interface{}, password interface{}) *MockService_CreateAdminUserIfNotExists_Call { + return &MockService_CreateAdminUserIfNotExists_Call{Call: _e.mock.On("CreateAdminUserIfNotExists", ctx, username, password)} +} + +func (_c *MockService_CreateAdminUserIfNotExists_Call) Run(run func(ctx context.Context, username string, password string)) *MockService_CreateAdminUserIfNotExists_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockService_CreateAdminUserIfNotExists_Call) Return(err error) *MockService_CreateAdminUserIfNotExists_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockService_CreateAdminUserIfNotExists_Call) RunAndReturn(run func(ctx context.Context, username string, password string) error) *MockService_CreateAdminUserIfNotExists_Call { + _c.Call.Return(run) + return _c +} + +// CreateNewUser provides a mock function for the type MockService +func (_mock *MockService) CreateNewUser(ctx context.Context, operator *Claims, username string, password string) error { + ret := _mock.Called(ctx, operator, username, password) + + if len(ret) == 0 { + panic("no return value specified for CreateNewUser") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *Claims, string, string) error); ok { + r0 = returnFunc(ctx, operator, username, password) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockService_CreateNewUser_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateNewUser' +type MockService_CreateNewUser_Call struct { + *mock.Call +} + +// CreateNewUser is a helper method to define mock.On call +// - ctx context.Context +// - operator *Claims +// - username string +// - password string +func (_e *MockService_Expecter) CreateNewUser(ctx interface{}, operator interface{}, username interface{}, password interface{}) *MockService_CreateNewUser_Call { + return &MockService_CreateNewUser_Call{Call: _e.mock.On("CreateNewUser", ctx, operator, username, password)} +} + +func (_c *MockService_CreateNewUser_Call) Run(run func(ctx context.Context, operator *Claims, username string, password string)) *MockService_CreateNewUser_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *Claims + if args[1] != nil { + arg1 = args[1].(*Claims) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 string + if args[3] != nil { + arg3 = args[3].(string) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *MockService_CreateNewUser_Call) Return(err error) *MockService_CreateNewUser_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockService_CreateNewUser_Call) RunAndReturn(run func(ctx context.Context, operator *Claims, username string, password string) error) *MockService_CreateNewUser_Call { + _c.Call.Return(run) + return _c +} + +// CreateRole provides a mock function for the type MockService +func (_mock *MockService) CreateRole(ctx context.Context, operator *Claims, role *Role) error { + ret := _mock.Called(ctx, operator, role) + + if len(ret) == 0 { + panic("no return value specified for CreateRole") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *Claims, *Role) error); ok { + r0 = returnFunc(ctx, operator, role) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockService_CreateRole_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateRole' +type MockService_CreateRole_Call struct { + *mock.Call +} + +// CreateRole is a helper method to define mock.On call +// - ctx context.Context +// - operator *Claims +// - role *Role +func (_e *MockService_Expecter) CreateRole(ctx interface{}, operator interface{}, role interface{}) *MockService_CreateRole_Call { + return &MockService_CreateRole_Call{Call: _e.mock.On("CreateRole", ctx, operator, role)} +} + +func (_c *MockService_CreateRole_Call) Run(run func(ctx context.Context, operator *Claims, role *Role)) *MockService_CreateRole_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *Claims + if args[1] != nil { + arg1 = args[1].(*Claims) + } + var arg2 *Role + if args[2] != nil { + arg2 = args[2].(*Role) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockService_CreateRole_Call) Return(err error) *MockService_CreateRole_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockService_CreateRole_Call) RunAndReturn(run func(ctx context.Context, operator *Claims, role *Role) error) *MockService_CreateRole_Call { + _c.Call.Return(run) + return _c +} + +// CreateScheduleStrategy provides a mock function for the type MockService +func (_mock *MockService) CreateScheduleStrategy(ctx context.Context, operator *Claims, strategy *ScheduleStrategy) error { + ret := _mock.Called(ctx, operator, strategy) + + if len(ret) == 0 { + panic("no return value specified for CreateScheduleStrategy") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *Claims, *ScheduleStrategy) error); ok { + r0 = returnFunc(ctx, operator, strategy) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockService_CreateScheduleStrategy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateScheduleStrategy' +type MockService_CreateScheduleStrategy_Call struct { + *mock.Call +} + +// CreateScheduleStrategy is a helper method to define mock.On call +// - ctx context.Context +// - operator *Claims +// - strategy *ScheduleStrategy +func (_e *MockService_Expecter) CreateScheduleStrategy(ctx interface{}, operator interface{}, strategy interface{}) *MockService_CreateScheduleStrategy_Call { + return &MockService_CreateScheduleStrategy_Call{Call: _e.mock.On("CreateScheduleStrategy", ctx, operator, strategy)} +} + +func (_c *MockService_CreateScheduleStrategy_Call) Run(run func(ctx context.Context, operator *Claims, strategy *ScheduleStrategy)) *MockService_CreateScheduleStrategy_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *Claims + if args[1] != nil { + arg1 = args[1].(*Claims) + } + var arg2 *ScheduleStrategy + if args[2] != nil { + arg2 = args[2].(*ScheduleStrategy) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockService_CreateScheduleStrategy_Call) Return(err error) *MockService_CreateScheduleStrategy_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockService_CreateScheduleStrategy_Call) RunAndReturn(run func(ctx context.Context, operator *Claims, strategy *ScheduleStrategy) error) *MockService_CreateScheduleStrategy_Call { + _c.Call.Return(run) + return _c +} + +// DeleteRole provides a mock function for the type MockService +func (_mock *MockService) DeleteRole(ctx context.Context, operator *Claims, roleID string) error { + ret := _mock.Called(ctx, operator, roleID) + + if len(ret) == 0 { + panic("no return value specified for DeleteRole") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *Claims, string) error); ok { + r0 = returnFunc(ctx, operator, roleID) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockService_DeleteRole_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteRole' +type MockService_DeleteRole_Call struct { + *mock.Call +} + +// DeleteRole is a helper method to define mock.On call +// - ctx context.Context +// - operator *Claims +// - roleID string +func (_e *MockService_Expecter) DeleteRole(ctx interface{}, operator interface{}, roleID interface{}) *MockService_DeleteRole_Call { + return &MockService_DeleteRole_Call{Call: _e.mock.On("DeleteRole", ctx, operator, roleID)} +} + +func (_c *MockService_DeleteRole_Call) Run(run func(ctx context.Context, operator *Claims, roleID string)) *MockService_DeleteRole_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *Claims + if args[1] != nil { + arg1 = args[1].(*Claims) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockService_DeleteRole_Call) Return(err error) *MockService_DeleteRole_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockService_DeleteRole_Call) RunAndReturn(run func(ctx context.Context, operator *Claims, roleID string) error) *MockService_DeleteRole_Call { + _c.Call.Return(run) + return _c +} + +// Login provides a mock function for the type MockService +func (_mock *MockService) Login(ctx context.Context, email string, password string) (string, error) { + ret := _mock.Called(ctx, email, password) + + if len(ret) == 0 { + panic("no return value specified for Login") + } + + var r0 string + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (string, error)); ok { + return returnFunc(ctx, email, password) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) string); ok { + r0 = returnFunc(ctx, email, password) + } else { + r0 = ret.Get(0).(string) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = returnFunc(ctx, email, password) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockService_Login_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Login' +type MockService_Login_Call struct { + *mock.Call +} + +// Login is a helper method to define mock.On call +// - ctx context.Context +// - email string +// - password string +func (_e *MockService_Expecter) Login(ctx interface{}, email interface{}, password interface{}) *MockService_Login_Call { + return &MockService_Login_Call{Call: _e.mock.On("Login", ctx, email, password)} +} + +func (_c *MockService_Login_Call) Run(run func(ctx context.Context, email string, password string)) *MockService_Login_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockService_Login_Call) Return(token string, err error) *MockService_Login_Call { + _c.Call.Return(token, err) + return _c +} + +func (_c *MockService_Login_Call) RunAndReturn(run func(ctx context.Context, email string, password string) (string, error)) *MockService_Login_Call { + _c.Call.Return(run) + return _c +} + +// QueryPermissions provides a mock function for the type MockService +func (_mock *MockService) QueryPermissions(ctx context.Context, opt *QueryPermissionOptions) error { + ret := _mock.Called(ctx, opt) + + if len(ret) == 0 { + panic("no return value specified for QueryPermissions") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *QueryPermissionOptions) error); ok { + r0 = returnFunc(ctx, opt) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockService_QueryPermissions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QueryPermissions' +type MockService_QueryPermissions_Call struct { + *mock.Call +} + +// QueryPermissions is a helper method to define mock.On call +// - ctx context.Context +// - opt *QueryPermissionOptions +func (_e *MockService_Expecter) QueryPermissions(ctx interface{}, opt interface{}) *MockService_QueryPermissions_Call { + return &MockService_QueryPermissions_Call{Call: _e.mock.On("QueryPermissions", ctx, opt)} +} + +func (_c *MockService_QueryPermissions_Call) Run(run func(ctx context.Context, opt *QueryPermissionOptions)) *MockService_QueryPermissions_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *QueryPermissionOptions + if args[1] != nil { + arg1 = args[1].(*QueryPermissionOptions) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockService_QueryPermissions_Call) Return(err error) *MockService_QueryPermissions_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockService_QueryPermissions_Call) RunAndReturn(run func(ctx context.Context, opt *QueryPermissionOptions) error) *MockService_QueryPermissions_Call { + _c.Call.Return(run) + return _c +} + +// QueryRoles provides a mock function for the type MockService +func (_mock *MockService) QueryRoles(ctx context.Context, opt *QueryRoleOptions) error { + ret := _mock.Called(ctx, opt) + + if len(ret) == 0 { + panic("no return value specified for QueryRoles") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *QueryRoleOptions) error); ok { + r0 = returnFunc(ctx, opt) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockService_QueryRoles_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QueryRoles' +type MockService_QueryRoles_Call struct { + *mock.Call +} + +// QueryRoles is a helper method to define mock.On call +// - ctx context.Context +// - opt *QueryRoleOptions +func (_e *MockService_Expecter) QueryRoles(ctx interface{}, opt interface{}) *MockService_QueryRoles_Call { + return &MockService_QueryRoles_Call{Call: _e.mock.On("QueryRoles", ctx, opt)} +} + +func (_c *MockService_QueryRoles_Call) Run(run func(ctx context.Context, opt *QueryRoleOptions)) *MockService_QueryRoles_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *QueryRoleOptions + if args[1] != nil { + arg1 = args[1].(*QueryRoleOptions) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockService_QueryRoles_Call) Return(err error) *MockService_QueryRoles_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockService_QueryRoles_Call) RunAndReturn(run func(ctx context.Context, opt *QueryRoleOptions) error) *MockService_QueryRoles_Call { + _c.Call.Return(run) + return _c +} + +// QueryUsers provides a mock function for the type MockService +func (_mock *MockService) QueryUsers(ctx context.Context, opt *QueryUserOptions) error { + ret := _mock.Called(ctx, opt) + + if len(ret) == 0 { + panic("no return value specified for QueryUsers") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *QueryUserOptions) error); ok { + r0 = returnFunc(ctx, opt) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockService_QueryUsers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QueryUsers' +type MockService_QueryUsers_Call struct { + *mock.Call +} + +// QueryUsers is a helper method to define mock.On call +// - ctx context.Context +// - opt *QueryUserOptions +func (_e *MockService_Expecter) QueryUsers(ctx interface{}, opt interface{}) *MockService_QueryUsers_Call { + return &MockService_QueryUsers_Call{Call: _e.mock.On("QueryUsers", ctx, opt)} +} + +func (_c *MockService_QueryUsers_Call) Run(run func(ctx context.Context, opt *QueryUserOptions)) *MockService_QueryUsers_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *QueryUserOptions + if args[1] != nil { + arg1 = args[1].(*QueryUserOptions) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockService_QueryUsers_Call) Return(err error) *MockService_QueryUsers_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockService_QueryUsers_Call) RunAndReturn(run func(ctx context.Context, opt *QueryUserOptions) error) *MockService_QueryUsers_Call { + _c.Call.Return(run) + return _c +} + +// ResetPassword provides a mock function for the type MockService +func (_mock *MockService) ResetPassword(ctx context.Context, operator *Claims, id string, newPassword string) error { + ret := _mock.Called(ctx, operator, id, newPassword) + + if len(ret) == 0 { + panic("no return value specified for ResetPassword") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *Claims, string, string) error); ok { + r0 = returnFunc(ctx, operator, id, newPassword) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockService_ResetPassword_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ResetPassword' +type MockService_ResetPassword_Call struct { + *mock.Call +} + +// ResetPassword is a helper method to define mock.On call +// - ctx context.Context +// - operator *Claims +// - id string +// - newPassword string +func (_e *MockService_Expecter) ResetPassword(ctx interface{}, operator interface{}, id interface{}, newPassword interface{}) *MockService_ResetPassword_Call { + return &MockService_ResetPassword_Call{Call: _e.mock.On("ResetPassword", ctx, operator, id, newPassword)} +} + +func (_c *MockService_ResetPassword_Call) Run(run func(ctx context.Context, operator *Claims, id string, newPassword string)) *MockService_ResetPassword_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *Claims + if args[1] != nil { + arg1 = args[1].(*Claims) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 string + if args[3] != nil { + arg3 = args[3].(string) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *MockService_ResetPassword_Call) Return(err error) *MockService_ResetPassword_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockService_ResetPassword_Call) RunAndReturn(run func(ctx context.Context, operator *Claims, id string, newPassword string) error) *MockService_ResetPassword_Call { + _c.Call.Return(run) + return _c +} + +// UpdateRole provides a mock function for the type MockService +func (_mock *MockService) UpdateRole(ctx context.Context, operator *Claims, roleID string, opt UpdateRoleOptions) error { + ret := _mock.Called(ctx, operator, roleID, opt) + + if len(ret) == 0 { + panic("no return value specified for UpdateRole") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *Claims, string, UpdateRoleOptions) error); ok { + r0 = returnFunc(ctx, operator, roleID, opt) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockService_UpdateRole_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateRole' +type MockService_UpdateRole_Call struct { + *mock.Call +} + +// UpdateRole is a helper method to define mock.On call +// - ctx context.Context +// - operator *Claims +// - roleID string +// - opt UpdateRoleOptions +func (_e *MockService_Expecter) UpdateRole(ctx interface{}, operator interface{}, roleID interface{}, opt interface{}) *MockService_UpdateRole_Call { + return &MockService_UpdateRole_Call{Call: _e.mock.On("UpdateRole", ctx, operator, roleID, opt)} +} + +func (_c *MockService_UpdateRole_Call) Run(run func(ctx context.Context, operator *Claims, roleID string, opt UpdateRoleOptions)) *MockService_UpdateRole_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *Claims + if args[1] != nil { + arg1 = args[1].(*Claims) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 UpdateRoleOptions + if args[3] != nil { + arg3 = args[3].(UpdateRoleOptions) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *MockService_UpdateRole_Call) Return(err error) *MockService_UpdateRole_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockService_UpdateRole_Call) RunAndReturn(run func(ctx context.Context, operator *Claims, roleID string, opt UpdateRoleOptions) error) *MockService_UpdateRole_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUserPermissions provides a mock function for the type MockService +func (_mock *MockService) UpdateUserPermissions(ctx context.Context, operator *Claims, id string, opt UpdateUserPermissionsOptions) error { + ret := _mock.Called(ctx, operator, id, opt) + + if len(ret) == 0 { + panic("no return value specified for UpdateUserPermissions") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *Claims, string, UpdateUserPermissionsOptions) error); ok { + r0 = returnFunc(ctx, operator, id, opt) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockService_UpdateUserPermissions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUserPermissions' +type MockService_UpdateUserPermissions_Call struct { + *mock.Call +} + +// UpdateUserPermissions is a helper method to define mock.On call +// - ctx context.Context +// - operator *Claims +// - id string +// - opt UpdateUserPermissionsOptions +func (_e *MockService_Expecter) UpdateUserPermissions(ctx interface{}, operator interface{}, id interface{}, opt interface{}) *MockService_UpdateUserPermissions_Call { + return &MockService_UpdateUserPermissions_Call{Call: _e.mock.On("UpdateUserPermissions", ctx, operator, id, opt)} +} + +func (_c *MockService_UpdateUserPermissions_Call) Run(run func(ctx context.Context, operator *Claims, id string, opt UpdateUserPermissionsOptions)) *MockService_UpdateUserPermissions_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *Claims + if args[1] != nil { + arg1 = args[1].(*Claims) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 UpdateUserPermissionsOptions + if args[3] != nil { + arg3 = args[3].(UpdateUserPermissionsOptions) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *MockService_UpdateUserPermissions_Call) Return(err error) *MockService_UpdateUserPermissions_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockService_UpdateUserPermissions_Call) RunAndReturn(run func(ctx context.Context, operator *Claims, id string, opt UpdateUserPermissionsOptions) error) *MockService_UpdateUserPermissions_Call { + _c.Call.Return(run) + return _c +} + +// VerifyJWTToken provides a mock function for the type MockService +func (_mock *MockService) VerifyJWTToken(ctx context.Context, tokenString string, permissionKey PermissionKey) (Claims, RolePolicy, error) { + ret := _mock.Called(ctx, tokenString, permissionKey) + + if len(ret) == 0 { + panic("no return value specified for VerifyJWTToken") + } + + var r0 Claims + var r1 RolePolicy + var r2 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, PermissionKey) (Claims, RolePolicy, error)); ok { + return returnFunc(ctx, tokenString, permissionKey) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, PermissionKey) Claims); ok { + r0 = returnFunc(ctx, tokenString, permissionKey) + } else { + r0 = ret.Get(0).(Claims) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, PermissionKey) RolePolicy); ok { + r1 = returnFunc(ctx, tokenString, permissionKey) + } else { + r1 = ret.Get(1).(RolePolicy) + } + if returnFunc, ok := ret.Get(2).(func(context.Context, string, PermissionKey) error); ok { + r2 = returnFunc(ctx, tokenString, permissionKey) + } else { + r2 = ret.Error(2) + } + return r0, r1, r2 +} + +// MockService_VerifyJWTToken_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VerifyJWTToken' +type MockService_VerifyJWTToken_Call struct { + *mock.Call +} + +// VerifyJWTToken is a helper method to define mock.On call +// - ctx context.Context +// - tokenString string +// - permissionKey PermissionKey +func (_e *MockService_Expecter) VerifyJWTToken(ctx interface{}, tokenString interface{}, permissionKey interface{}) *MockService_VerifyJWTToken_Call { + return &MockService_VerifyJWTToken_Call{Call: _e.mock.On("VerifyJWTToken", ctx, tokenString, permissionKey)} +} + +func (_c *MockService_VerifyJWTToken_Call) Run(run func(ctx context.Context, tokenString string, permissionKey PermissionKey)) *MockService_VerifyJWTToken_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 PermissionKey + if args[2] != nil { + arg2 = args[2].(PermissionKey) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockService_VerifyJWTToken_Call) Return(claims Claims, rolePolicy RolePolicy, err error) *MockService_VerifyJWTToken_Call { + _c.Call.Return(claims, rolePolicy, err) + return _c +} + +func (_c *MockService_VerifyJWTToken_Call) RunAndReturn(run func(ctx context.Context, tokenString string, permissionKey PermissionKey) (Claims, RolePolicy, error)) *MockService_VerifyJWTToken_Call { + _c.Call.Return(run) + return _c +} + +// NewMockK8SAdapter creates a new instance of MockK8SAdapter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockK8SAdapter(t interface { + mock.TestingT + Cleanup(func()) +}) *MockK8SAdapter { + mock := &MockK8SAdapter{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockK8SAdapter is an autogenerated mock type for the K8SAdapter type +type MockK8SAdapter struct { + mock.Mock +} + +type MockK8SAdapter_Expecter struct { + mock *mock.Mock +} + +func (_m *MockK8SAdapter) EXPECT() *MockK8SAdapter_Expecter { + return &MockK8SAdapter_Expecter{mock: &_m.Mock} +} + +// QueryDecisionMakerPods provides a mock function for the type MockK8SAdapter +func (_mock *MockK8SAdapter) QueryDecisionMakerPods(ctx context.Context, opt *QueryDecisionMakerPodsOptions) ([]*DecisionMakerPod, error) { + ret := _mock.Called(ctx, opt) + + if len(ret) == 0 { + panic("no return value specified for QueryDecisionMakerPods") + } + + var r0 []*DecisionMakerPod + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *QueryDecisionMakerPodsOptions) ([]*DecisionMakerPod, error)); ok { + return returnFunc(ctx, opt) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, *QueryDecisionMakerPodsOptions) []*DecisionMakerPod); ok { + r0 = returnFunc(ctx, opt) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*DecisionMakerPod) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, *QueryDecisionMakerPodsOptions) error); ok { + r1 = returnFunc(ctx, opt) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockK8SAdapter_QueryDecisionMakerPods_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QueryDecisionMakerPods' +type MockK8SAdapter_QueryDecisionMakerPods_Call struct { + *mock.Call +} + +// QueryDecisionMakerPods is a helper method to define mock.On call +// - ctx context.Context +// - opt *QueryDecisionMakerPodsOptions +func (_e *MockK8SAdapter_Expecter) QueryDecisionMakerPods(ctx interface{}, opt interface{}) *MockK8SAdapter_QueryDecisionMakerPods_Call { + return &MockK8SAdapter_QueryDecisionMakerPods_Call{Call: _e.mock.On("QueryDecisionMakerPods", ctx, opt)} +} + +func (_c *MockK8SAdapter_QueryDecisionMakerPods_Call) Run(run func(ctx context.Context, opt *QueryDecisionMakerPodsOptions)) *MockK8SAdapter_QueryDecisionMakerPods_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *QueryDecisionMakerPodsOptions + if args[1] != nil { + arg1 = args[1].(*QueryDecisionMakerPodsOptions) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockK8SAdapter_QueryDecisionMakerPods_Call) Return(decisionMakerPods []*DecisionMakerPod, err error) *MockK8SAdapter_QueryDecisionMakerPods_Call { + _c.Call.Return(decisionMakerPods, err) + return _c +} + +func (_c *MockK8SAdapter_QueryDecisionMakerPods_Call) RunAndReturn(run func(ctx context.Context, opt *QueryDecisionMakerPodsOptions) ([]*DecisionMakerPod, error)) *MockK8SAdapter_QueryDecisionMakerPods_Call { + _c.Call.Return(run) + return _c +} + +// QueryPods provides a mock function for the type MockK8SAdapter +func (_mock *MockK8SAdapter) QueryPods(ctx context.Context, opt *QueryPodsOptions) ([]*Pod, error) { + ret := _mock.Called(ctx, opt) + + if len(ret) == 0 { + panic("no return value specified for QueryPods") + } + + var r0 []*Pod + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *QueryPodsOptions) ([]*Pod, error)); ok { + return returnFunc(ctx, opt) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, *QueryPodsOptions) []*Pod); ok { + r0 = returnFunc(ctx, opt) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*Pod) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, *QueryPodsOptions) error); ok { + r1 = returnFunc(ctx, opt) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockK8SAdapter_QueryPods_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QueryPods' +type MockK8SAdapter_QueryPods_Call struct { + *mock.Call +} + +// QueryPods is a helper method to define mock.On call +// - ctx context.Context +// - opt *QueryPodsOptions +func (_e *MockK8SAdapter_Expecter) QueryPods(ctx interface{}, opt interface{}) *MockK8SAdapter_QueryPods_Call { + return &MockK8SAdapter_QueryPods_Call{Call: _e.mock.On("QueryPods", ctx, opt)} +} + +func (_c *MockK8SAdapter_QueryPods_Call) Run(run func(ctx context.Context, opt *QueryPodsOptions)) *MockK8SAdapter_QueryPods_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *QueryPodsOptions + if args[1] != nil { + arg1 = args[1].(*QueryPodsOptions) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockK8SAdapter_QueryPods_Call) Return(pods []*Pod, err error) *MockK8SAdapter_QueryPods_Call { + _c.Call.Return(pods, err) + return _c +} + +func (_c *MockK8SAdapter_QueryPods_Call) RunAndReturn(run func(ctx context.Context, opt *QueryPodsOptions) ([]*Pod, error)) *MockK8SAdapter_QueryPods_Call { + _c.Call.Return(run) + return _c +} + +// NewMockDecisionMakerAdapter creates a new instance of MockDecisionMakerAdapter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockDecisionMakerAdapter(t interface { + mock.TestingT + Cleanup(func()) +}) *MockDecisionMakerAdapter { + mock := &MockDecisionMakerAdapter{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockDecisionMakerAdapter is an autogenerated mock type for the DecisionMakerAdapter type +type MockDecisionMakerAdapter struct { + mock.Mock +} + +type MockDecisionMakerAdapter_Expecter struct { + mock *mock.Mock +} + +func (_m *MockDecisionMakerAdapter) EXPECT() *MockDecisionMakerAdapter_Expecter { + return &MockDecisionMakerAdapter_Expecter{mock: &_m.Mock} +} + +// SendSchedulingIntent provides a mock function for the type MockDecisionMakerAdapter +func (_mock *MockDecisionMakerAdapter) SendSchedulingIntent(ctx context.Context, decisionMaker *DecisionMakerPod, intents []*ScheduleIntent) error { + ret := _mock.Called(ctx, decisionMaker, intents) + + if len(ret) == 0 { + panic("no return value specified for SendSchedulingIntent") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *DecisionMakerPod, []*ScheduleIntent) error); ok { + r0 = returnFunc(ctx, decisionMaker, intents) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockDecisionMakerAdapter_SendSchedulingIntent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendSchedulingIntent' +type MockDecisionMakerAdapter_SendSchedulingIntent_Call struct { + *mock.Call +} + +// SendSchedulingIntent is a helper method to define mock.On call +// - ctx context.Context +// - decisionMaker *DecisionMakerPod +// - intents []*ScheduleIntent +func (_e *MockDecisionMakerAdapter_Expecter) SendSchedulingIntent(ctx interface{}, decisionMaker interface{}, intents interface{}) *MockDecisionMakerAdapter_SendSchedulingIntent_Call { + return &MockDecisionMakerAdapter_SendSchedulingIntent_Call{Call: _e.mock.On("SendSchedulingIntent", ctx, decisionMaker, intents)} +} + +func (_c *MockDecisionMakerAdapter_SendSchedulingIntent_Call) Run(run func(ctx context.Context, decisionMaker *DecisionMakerPod, intents []*ScheduleIntent)) *MockDecisionMakerAdapter_SendSchedulingIntent_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *DecisionMakerPod + if args[1] != nil { + arg1 = args[1].(*DecisionMakerPod) + } + var arg2 []*ScheduleIntent + if args[2] != nil { + arg2 = args[2].([]*ScheduleIntent) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockDecisionMakerAdapter_SendSchedulingIntent_Call) Return(err error) *MockDecisionMakerAdapter_SendSchedulingIntent_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockDecisionMakerAdapter_SendSchedulingIntent_Call) RunAndReturn(run func(ctx context.Context, decisionMaker *DecisionMakerPod, intents []*ScheduleIntent) error) *MockDecisionMakerAdapter_SendSchedulingIntent_Call { + _c.Call.Return(run) + return _c +} diff --git a/manager/domain/strategy.go b/manager/domain/strategy.go index 70ac948..0978dc2 100644 --- a/manager/domain/strategy.go +++ b/manager/domain/strategy.go @@ -1,30 +1,46 @@ package domain -import "go.mongodb.org/mongo-driver/v2/bson" +import ( + "github.com/Gthulhu/api/pkg/util" + "go.mongodb.org/mongo-driver/v2/bson" +) type ScheduleStrategy struct { - BaseEntity + BaseEntity `bson:",inline"` StrategyNamespace string `bson:"strategyNamespace,omitempty"` LabelSelectors []LabelSelector `bson:"labelSelectors,omitempty"` - K8sNamespaces []string `bson:"k8sNamespaces,omitempty"` + K8sNamespace []string `bson:"k8sNamespace,omitempty"` CommandRegex string `bson:"commandRegex,omitempty"` Priority int `bson:"priority,omitempty"` ExecutionTime int64 `bson:"executionTime,omitempty"` } +func NewScheduleIntent(strategy *ScheduleStrategy, pod *Pod) ScheduleIntent { + return ScheduleIntent{ + BaseEntity: NewBaseEntity(util.Ptr(strategy.CreatorID), util.Ptr(strategy.UpdaterID)), + StrategyID: strategy.ID, + PodID: pod.PodID, + NodeID: pod.NodeID, + K8sNamespace: pod.K8SNamespace, + CommandRegex: strategy.CommandRegex, + Priority: strategy.Priority, + ExecutionTime: strategy.ExecutionTime, + PodLabels: pod.Labels, + State: IntentStateInitialized, + } +} + type ScheduleIntent struct { - ID bson.ObjectID `bson:"_id,omitempty"` - StrategyID bson.ObjectID `bson:"strategyID,omitempty"` - PodID string `bson:"podID,omitempty"` - NodeID string `bson:"nodeID,omitempty"` - K8sNamespace string `bson:"k8sNamespace,omitempty"` - CommandRegex string `bson:"commandRegex,omitempty"` - Priority int `bson:"priority,omitempty"` - ExecutionTime int64 `bson:"executionTime,omitempty"` - LabelSelectors []LabelSelector `bson:"labelSelectors,omitempty"` - State IntentState `bson:"state,omitempty"` - CreatedTime int64 `bson:"createdTime,omitempty"` - SentTime int64 `bson:"sentTime,omitempty"` + BaseEntity `bson:",inline"` + StrategyID bson.ObjectID `bson:"strategyID,omitempty"` + PodID string `bson:"podID,omitempty"` + NodeID string `bson:"nodeID,omitempty"` + K8sNamespace string `bson:"k8sNamespace,omitempty"` + CommandRegex string `bson:"commandRegex,omitempty"` + Priority int `bson:"priority,omitempty"` + ExecutionTime int64 `bson:"executionTime,omitempty"` + PodLabels map[string]string `bson:"podLabels,omitempty"` + State IntentState `bson:"state,omitempty"` } type LabelSelector struct { diff --git a/manager/k8s_adapter/adapter.go b/manager/k8s_adapter/adapter.go index d545d90..1c33ee4 100644 --- a/manager/k8s_adapter/adapter.go +++ b/manager/k8s_adapter/adapter.go @@ -132,16 +132,14 @@ func (a *Adapter) StopPodWatcher() { }) } -func (a *Adapter) QueryPods(ctx context.Context, opt *domain.QueryPodsOptions) error { +func (a *Adapter) QueryPods(ctx context.Context, opt *domain.QueryPodsOptions) ([]*domain.Pod, error) { if opt == nil { - return domain.ErrNilQueryInput + return nil, domain.ErrNilQueryInput } if a == nil || a.client == nil { - return domain.ErrNoClient + return nil, domain.ErrNoClient } - opt.Result = opt.Result[:0] - labelSelector := buildLabelSelector(opt.LabelSelectors) namespaces := opt.K8SNamespace if len(namespaces) == 0 { @@ -152,15 +150,16 @@ func (a *Adapter) QueryPods(ctx context.Context, opt *domain.QueryPodsOptions) e if opt.CommandRegex != "" { re, err := regexp.Compile(opt.CommandRegex) if err != nil { - return fmt.Errorf("compile command regex: %w", err) + return nil, fmt.Errorf("compile command regex: %w", err) } cmdRegex = re } pods, err := a.listPods(ctx, namespaces, labelSelector) if err != nil { - return err + return nil, err } + results := make([]*domain.Pod, 0, len(pods)) for _, pod := range pods { containers := buildContainers(pod, cmdRegex) @@ -176,23 +175,21 @@ func (a *Adapter) QueryPods(ctx context.Context, opt *domain.QueryPodsOptions) e NodeID: pod.Spec.NodeName, Containers: containers, } - opt.Result = append(opt.Result, result) + results = append(results, result) } - return nil + return results, nil } -func (a *Adapter) QueryDecisionMakerPods(ctx context.Context, opt *domain.QueryDecisionMakerPodsOptions) error { +func (a *Adapter) QueryDecisionMakerPods(ctx context.Context, opt *domain.QueryDecisionMakerPodsOptions) ([]*domain.DecisionMakerPod, error) { if opt == nil { - return domain.ErrNilQueryInput + return nil, domain.ErrNilQueryInput } if a == nil || a.client == nil { - return domain.ErrNoClient + return nil, domain.ErrNoClient } - opt.Result = opt.Result[:0] - labelSelector := buildLabelSelector([]domain.LabelSelector{opt.DecisionMakerLabel}) namespaces := opt.K8SNamespace if len(namespaces) == 0 { @@ -206,9 +203,9 @@ func (a *Adapter) QueryDecisionMakerPods(ctx context.Context, opt *domain.QueryD pods, err := a.listPods(ctx, namespaces, labelSelector) if err != nil { - return err + return nil, err } - + results := make([]*domain.DecisionMakerPod, 0, len(pods)) for _, pod := range pods { if len(nodeFilters) > 0 { if _, ok := nodeFilters[pod.Spec.NodeName]; !ok { @@ -221,7 +218,7 @@ func (a *Adapter) QueryDecisionMakerPods(ctx context.Context, opt *domain.QueryD host = pod.Status.HostIP } - opt.Result = append(opt.Result, &domain.DecisionMakerPod{ + results = append(results, &domain.DecisionMakerPod{ NodeID: pod.Spec.NodeName, Port: firstContainerPort(pod), Host: host, @@ -229,7 +226,7 @@ func (a *Adapter) QueryDecisionMakerPods(ctx context.Context, opt *domain.QueryD }) } - return nil + return results, nil } func (a *Adapter) listPods(ctx context.Context, namespaces []string, labelSelector string) ([]apiv1.Pod, error) { @@ -246,7 +243,7 @@ func (a *Adapter) listPods(ctx context.Context, namespaces []string, labelSelect } func (a *Adapter) podsFromCache(namespaces []string, selector labels.Selector) []apiv1.Pod { - nsAll := len(namespaces) == 1 && namespaces[0] == metav1.NamespaceAll + nsAll := len(namespaces) == 0 || (len(namespaces) == 1 && namespaces[0] == metav1.NamespaceAll) nsSet := make(map[string]struct{}, len(namespaces)) for _, ns := range namespaces { nsSet[ns] = struct{}{} diff --git a/manager/k8s_adapter/adapter_local_test.go b/manager/k8s_adapter/adapter_local_test.go index 9fe03ab..6380ca3 100644 --- a/manager/k8s_adapter/adapter_local_test.go +++ b/manager/k8s_adapter/adapter_local_test.go @@ -124,11 +124,12 @@ func TestQueryPodsWithLocalKubeconfig(t *testing.T) { } waitForLocal(t, func(ctx context.Context) (bool, error) { - opt.Result = opt.Result[:0] - if err := adapter.QueryPods(ctx, opt); err != nil { + + results, err := adapter.QueryPods(ctx, opt) + if err != nil { return false, err } - return len(opt.Result) == 1, nil + return len(results) == 1, nil }) { /*** Test Modifying Pod ***/ @@ -138,11 +139,12 @@ func TestQueryPodsWithLocalKubeconfig(t *testing.T) { opt.LabelSelectors = []domain.LabelSelector{{Key: "app", Value: "demo2"}} waitForLocal(t, func(ctx context.Context) (bool, error) { - opt.Result = opt.Result[:0] - if err := adapter.QueryPods(ctx, opt); err != nil { + + results, err := adapter.QueryPods(ctx, opt) + if err != nil { return false, err } - return len(opt.Result) == 1 && opt.Result[0].Labels["app"] == "demo2", nil + return len(results) == 1 && results[0].Labels["app"] == "demo2", nil }) } @@ -152,11 +154,12 @@ func TestQueryPodsWithLocalKubeconfig(t *testing.T) { } waitForLocal(t, func(ctx context.Context) (bool, error) { - opt.Result = opt.Result[:0] - if err := adapter.QueryPods(ctx, opt); err != nil { + + results, err := adapter.QueryPods(ctx, opt) + if err != nil { return false, err } - return len(opt.Result) == 0, nil + return len(results) == 0, nil }) } } @@ -254,14 +257,14 @@ func TestQueryDecisionMakerPodsWithLocalKubeconfig(t *testing.T) { } waitForLocal(t, func(ctx context.Context) (bool, error) { - opt.Result = opt.Result[:0] - if err := adapter.QueryDecisionMakerPods(ctx, opt); err != nil { + results, err := adapter.QueryDecisionMakerPods(ctx, opt) + if err != nil { return false, err } - if len(opt.Result) != 1 { + if len(results) != 1 { return false, nil } - return opt.Result[0].NodeID != "" && opt.Result[0].Host != "", nil + return results[0].NodeID != "" && results[0].Host != "", nil }) { /*** Test Modifying DecisionMaker Pod ***/ @@ -271,14 +274,15 @@ func TestQueryDecisionMakerPodsWithLocalKubeconfig(t *testing.T) { opt.DecisionMakerLabel = domain.LabelSelector{Key: "dm", Value: "dm2"} waitForLocal(t, func(ctx context.Context) (bool, error) { - opt.Result = opt.Result[:0] - if err := adapter.QueryDecisionMakerPods(ctx, opt); err != nil { + + results, err := adapter.QueryDecisionMakerPods(ctx, opt) + if err != nil { return false, err } - if len(opt.Result) != 1 { + if len(results) != 1 { return false, nil } - return opt.Result[0].Host != "" && opt.Result[0].NodeID != "" && opt.Result[0].Port == 8080, nil + return results[0].Host != "" && results[0].NodeID != "" && results[0].Port == 8080, nil }) } @@ -288,11 +292,11 @@ func TestQueryDecisionMakerPodsWithLocalKubeconfig(t *testing.T) { } waitForLocal(t, func(ctx context.Context) (bool, error) { - opt.Result = opt.Result[:0] - if err := adapter.QueryDecisionMakerPods(ctx, opt); err != nil { + results, err := adapter.QueryDecisionMakerPods(ctx, opt) + if err != nil { return false, err } - return len(opt.Result) == 0, nil + return len(results) == 0, nil }) } } diff --git a/manager/k8s_adapter/adapter_test.go b/manager/k8s_adapter/adapter_test.go index 9f0e2da..20a4bd1 100644 --- a/manager/k8s_adapter/adapter_test.go +++ b/manager/k8s_adapter/adapter_test.go @@ -71,14 +71,14 @@ func TestPodWatcherCacheLifecycle(t *testing.T) { }, } - if err := adapter.QueryPods(context.Background(), opt); err != nil { + results, err := adapter.QueryPods(context.Background(), opt) + if err != nil { t.Fatalf("query pods after create: %v", err) + } else if len(results) != 1 { + t.Fatalf("expected 1 pod after create, got %d", len(results)) } - if len(opt.Result) != 1 { - t.Fatalf("expected 1 pod after create, got %d", len(opt.Result)) - } - if opt.Result[0].Containers[0].ContainerID != "docker://abc" { - t.Fatalf("unexpected container ID: %s", opt.Result[0].Containers[0].ContainerID) + if results[0].Containers[0].ContainerID != "docker://abc" { + t.Fatalf("unexpected container ID: %s", results[0].Containers[0].ContainerID) } } @@ -101,11 +101,12 @@ func TestPodWatcherCacheLifecycle(t *testing.T) { {Key: "app", Value: "demo2"}, }, } - if err := adapter.QueryPods(context.Background(), opt); err != nil { + results, err := adapter.QueryPods(context.Background(), opt) + if err != nil { t.Fatalf("query pods after update: %v", err) } - if len(opt.Result) != 1 { - t.Fatalf("expected 1 pod after update, got %d", len(opt.Result)) + if len(results) != 1 { + t.Fatalf("expected 1 pod after update, got %d", len(results)) } } @@ -127,11 +128,12 @@ func TestPodWatcherCacheLifecycle(t *testing.T) { {Key: "app", Value: "demo2"}, }, } - if err := adapter.QueryPods(context.Background(), opt); err != nil { + results, err := adapter.QueryPods(context.Background(), opt) + if err != nil { t.Fatalf("query pods after delete: %v", err) } - if len(opt.Result) != 0 { - t.Fatalf("expected no pod after delete, got %d", len(opt.Result)) + if len(results) != 0 { + t.Fatalf("expected no pod after delete, got %d", len(results)) } } } @@ -193,15 +195,16 @@ func TestQueryPodsUsesCache(t *testing.T) { }, } - if err := adapter.QueryPods(context.Background(), opt); err != nil { + results, err := adapter.QueryPods(context.Background(), opt) + if err != nil { t.Fatalf("QueryPods returned error: %v", err) } - if len(opt.Result) != 1 { - t.Fatalf("expected 1 pod, got %d", len(opt.Result)) + if len(results) != 1 { + t.Fatalf("expected 1 pod, got %d", len(results)) } - got := opt.Result[0] + got := results[0] if got.PodID != "uid-1" { t.Fatalf("unexpected PodID %q", got.PodID) } @@ -256,15 +259,16 @@ func TestQueryDecisionMakerPodsUsesCache(t *testing.T) { }, } - if err := adapter.QueryDecisionMakerPods(context.Background(), opt); err != nil { + results, err := adapter.QueryDecisionMakerPods(context.Background(), opt) + if err != nil { t.Fatalf("QueryDecisionMakerPods returned error: %v", err) } - if len(opt.Result) != 1 { - t.Fatalf("expected 1 decision maker pod, got %d", len(opt.Result)) + if len(results) != 1 { + t.Fatalf("expected 1 decision maker pod, got %d", len(results)) } - got := opt.Result[0] + got := results[0] if got.NodeID != "node-1" { t.Fatalf("unexpected NodeID %q", got.NodeID) } diff --git a/manager/migration/001_init_collections.down.json b/manager/migration/001_init_collections.down.json index 03c7d0c..e7c92a1 100644 --- a/manager/migration/001_init_collections.down.json +++ b/manager/migration/001_init_collections.down.json @@ -1,5 +1,7 @@ [ { "drop": "users" }, { "drop": "roles" }, - { "drop": "permissions" } + { "drop": "permissions" }, + { "drop": "schedule_strategies" }, + { "drop": "schedule_intents" } ] diff --git a/manager/migration/001_init_collections.up.json b/manager/migration/001_init_collections.up.json index d7d6b97..f0427a3 100644 --- a/manager/migration/001_init_collections.up.json +++ b/manager/migration/001_init_collections.up.json @@ -178,5 +178,123 @@ "name": "idx_permissions_key_unique" } ] + }, + { + "create": "schedule_strategies", + "validator": { + "$jsonSchema": { + "bsonType": "object", + "properties": { + "_id": { + "bsonType": "objectId" + }, + "createdTime": { + "bsonType": "long" + }, + "updatedTime": { + "bsonType": "long" + }, + "deletedTime": { + "bsonType": "long" + }, + "creatorID": { + "bsonType": "objectId" + }, + "updaterID": { + "bsonType": "objectId" + }, + "strategyNamespace": { + "bsonType": "string" + }, + "labelSelectors": { + "bsonType": "array", + "items": { + "bsonType": "object", + "properties": { + "key": { + "bsonType": "string" + }, + "value": { + "bsonType": "string" + } + } + } + }, + "k8sNamespace": { + "bsonType": "array", + "items": { + "bsonType": "string" + } + }, + "commandRegex": { + "bsonType": "string" + }, + "priority": { + "bsonType": "int" + }, + "executionTime": { + "bsonType": "long" + } + } + } + } + }, + { + "create": "schedule_intents", + "validator": { + "$jsonSchema": { + "bsonType": "object", + "properties": { + "_id": { + "bsonType": "objectId" + }, + "createdTime": { + "bsonType": "long" + }, + "updatedTime": { + "bsonType": "long" + }, + "deletedTime": { + "bsonType": "long" + }, + "creatorID": { + "bsonType": "objectId" + }, + "updaterID": { + "bsonType": "objectId" + }, + "strategyID": { + "bsonType": "objectId" + }, + "podID": { + "bsonType": "string" + }, + "nodeID": { + "bsonType": "string" + }, + "k8sNamespace": { + "bsonType": "string" + }, + "commandRegex": { + "bsonType": "string" + }, + "priority": { + "bsonType": "int" + }, + "executionTime": { + "bsonType": "long" + }, + "podLabels": { + "bsonType": "object", + "additionalProperties": { + "bsonType": "string" + } + }, + "state": { + "bsonType": "int" + } + } + } + } } ] \ No newline at end of file diff --git a/manager/migration/002_init_default_permissions.up.json b/manager/migration/002_init_default_permissions.up.json index a616560..6b96154 100644 --- a/manager/migration/002_init_default_permissions.up.json +++ b/manager/migration/002_init_default_permissions.up.json @@ -55,6 +55,24 @@ "resource": "permission", "action": "read", "description": "Read permissions" + }, + { + "key": "schedule_strategy.read", + "resource": "schedule_strategy", + "action": "read", + "description": "Read schedule strategies" + }, + { + "key": "schedule_strategy.create", + "resource": "schedule_strategy", + "action": "create", + "description": "Create schedule strategies" + }, + { + "key": "schedule_intent.read", + "resource": "schedule_intent", + "action": "read", + "description": "Read schedule intents" } ] } diff --git a/manager/migration/003_init_default_roles.up.json b/manager/migration/003_init_default_roles.up.json index 41187c3..3068bd5 100644 --- a/manager/migration/003_init_default_roles.up.json +++ b/manager/migration/003_init_default_roles.up.json @@ -14,7 +14,10 @@ { "permissionKey": "role.read", "self": false }, { "permissionKey": "role.update", "self": false }, { "permissionKey": "role.delete", "self": false }, - { "permissionKey": "permission.read", "self": false } + { "permissionKey": "permission.read", "self": false }, + { "permissionKey": "schedule_strategy.create", "self": false }, + { "permissionKey": "schedule_strategy.read", "self": false }, + { "permissionKey": "schedule_intent.read", "self": false } ], "created_time": { "$numberLong": "0" }, "updated_time": { "$numberLong": "0" } diff --git a/manager/repository/repo.go b/manager/repository/repo.go index 663c741..155c819 100644 --- a/manager/repository/repo.go +++ b/manager/repository/repo.go @@ -4,17 +4,14 @@ import ( "context" "crypto/tls" "crypto/x509" - "errors" "fmt" "time" "github.com/Gthulhu/api/config" "github.com/Gthulhu/api/manager/domain" - "go.uber.org/fx" - - "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" + "go.uber.org/fx" ) type Params struct { @@ -66,268 +63,11 @@ type repo struct { } const ( - userCollection = "users" - roleCollection = "roles" - permissionCollection = "permissions" - auditLogCollection = "audit_logs" - defaultTimestampField = "timestamp" + userCollection = "users" + roleCollection = "roles" + permissionCollection = "permissions" + auditLogCollection = "audit_logs" + defaultTimestampField = "timestamp" + scheduleStrategyCollection = "schedule_strategies" + scheduleIntentCollection = "schedule_intents" ) - -func (r *repo) CreateUser(ctx context.Context, user *domain.User) error { - if user == nil { - return errors.New("nil user") - } - - now := time.Now().UnixMilli() - if user.ID.IsZero() { - user.ID = bson.NewObjectID() - } - if user.CreatedTime == 0 { - user.CreatedTime = now - } - user.UpdatedTime = now - - res, err := r.db.Collection(userCollection).InsertOne(ctx, user) - if err != nil { - return fmt.Errorf("create user, err: %w", err) - } - if oid, ok := res.InsertedID.(bson.ObjectID); ok { - user.ID = oid - } - return nil -} - -func (r *repo) UpdateUser(ctx context.Context, user *domain.User) error { - if user == nil { - return errors.New("nil user") - } - if user.ID.IsZero() { - return errors.New("user id is required") - } - - user.UpdatedTime = time.Now().UnixMilli() - res, err := r.db.Collection(userCollection).ReplaceOne(ctx, bson.M{"_id": user.ID}, user) - if err != nil { - return fmt.Errorf("update user, err: %w", err) - } - if res.MatchedCount == 0 { - return domain.ErrNotFound - } - return nil -} - -func (r *repo) QueryUsers(ctx context.Context, opt *domain.QueryUserOptions) error { - if opt == nil { - return errors.New("nil query options") - } - - filter := bson.M{} - if len(opt.IDs) > 0 { - filter["_id"] = bson.M{"$in": opt.IDs} - } - if len(opt.UserNames) > 0 { - filter["username"] = bson.M{"$in": opt.UserNames} - } - - cursor, err := r.db.Collection(userCollection).Find(ctx, filter) - if err != nil { - return fmt.Errorf("find users, err: %w", err) - } - - var result []*domain.User - if err := cursor.All(ctx, &result); err != nil { - return fmt.Errorf("decode users, err: %w", err) - } - opt.Result = result - return nil -} - -func (r *repo) CreateRole(ctx context.Context, role *domain.Role) error { - if role == nil { - return errors.New("nil role") - } - - now := time.Now().UnixMilli() - if role.ID.IsZero() { - role.ID = bson.NewObjectID() - } - if role.CreatedTime == 0 { - role.CreatedTime = now - } - role.UpdatedTime = now - - res, err := r.db.Collection(roleCollection).InsertOne(ctx, role) - if err != nil { - return fmt.Errorf("create role, err: %w", err) - } - if oid, ok := res.InsertedID.(bson.ObjectID); ok { - role.ID = oid - } - return nil -} - -func (r *repo) UpdateRole(ctx context.Context, role *domain.Role) error { - if role == nil { - return errors.New("nil role") - } - if role.ID.IsZero() { - return errors.New("role id is required") - } - - role.UpdatedTime = time.Now().UnixMilli() - res, err := r.db.Collection(roleCollection).ReplaceOne(ctx, bson.M{"_id": role.ID}, role) - if err != nil { - return fmt.Errorf("update role, err: %w", err) - } - if res.MatchedCount == 0 { - return domain.ErrNotFound - } - return nil -} - -func (r *repo) QueryRoles(ctx context.Context, opt *domain.QueryRoleOptions) error { - if opt == nil { - return errors.New("nil query options") - } - - filter := bson.M{} - if len(opt.IDs) > 0 { - filter["_id"] = bson.M{"$in": opt.IDs} - } - if len(opt.Names) > 0 { - filter["name"] = bson.M{"$in": opt.Names} - } - - cursor, err := r.db.Collection(roleCollection).Find(ctx, filter) - if err != nil { - return fmt.Errorf("find roles, err: %w", err) - } - - var result []*domain.Role - if err := cursor.All(ctx, &result); err != nil { - return fmt.Errorf("decode roles, err: %w", err) - } - opt.Result = result - return nil -} - -func (r *repo) CreatePermission(ctx context.Context, permission *domain.Permission) error { - if permission == nil { - return errors.New("nil permission") - } - - if permission.ID.IsZero() { - permission.ID = bson.NewObjectID() - } - - res, err := r.db.Collection(permissionCollection).InsertOne(ctx, permission) - if err != nil { - return fmt.Errorf("create permission, err: %w", err) - } - if oid, ok := res.InsertedID.(bson.ObjectID); ok { - permission.ID = oid - } - return nil -} - -func (r *repo) UpdatePermission(ctx context.Context, permission *domain.Permission) error { - if permission == nil { - return errors.New("nil permission") - } - if permission.ID.IsZero() { - return errors.New("permission id is required") - } - - res, err := r.db.Collection(permissionCollection).ReplaceOne(ctx, bson.M{"_id": permission.ID}, permission) - if err != nil { - return fmt.Errorf("update permission, err: %w", err) - } - if res.MatchedCount == 0 { - return domain.ErrNotFound - } - return nil -} - -func (r *repo) QueryPermissions(ctx context.Context, opt *domain.QueryPermissionOptions) error { - if opt == nil { - return errors.New("nil query options") - } - - filter := bson.M{} - if len(opt.IDs) > 0 { - filter["_id"] = bson.M{"$in": opt.IDs} - } - if len(opt.Keys) > 0 { - filter["key"] = bson.M{"$in": opt.Keys} - } - if len(opt.Resources) > 0 { - filter["resource"] = bson.M{"$in": opt.Resources} - } - - cursor, err := r.db.Collection(permissionCollection).Find(ctx, filter) - if err != nil { - return fmt.Errorf("find permissions, err: %w", err) - } - - var result []*domain.Permission - if err := cursor.All(ctx, &result); err != nil { - return fmt.Errorf("decode permissions, err: %w", err) - } - opt.Result = result - return nil -} - -func (r *repo) CreateAuditLog(ctx context.Context, log *domain.AuditLog) error { - if log == nil { - return errors.New("nil audit log") - } - if log.ID.IsZero() { - log.ID = bson.NewObjectID() - } - if log.Timestamp == 0 { - log.Timestamp = time.Now().UnixMilli() - } - - res, err := r.db.Collection(auditLogCollection).InsertOne(ctx, log) - if err != nil { - return fmt.Errorf("create audit log, err: %w", err) - } - if oid, ok := res.InsertedID.(bson.ObjectID); ok { - log.ID = oid - } - return nil -} - -func (r *repo) QueryAuditLogs(ctx context.Context, opt *domain.QueryAuditLogOptions) error { - if opt == nil { - return errors.New("nil query options") - } - - filter := bson.M{} - if len(opt.UserIDs) > 0 { - filter["user_id"] = bson.M{"$in": opt.UserIDs} - } - - if opt.TimestampGTE > 0 || opt.TimestampLTE > 0 { - timeFilter := bson.M{} - if opt.TimestampGTE > 0 { - timeFilter["$gte"] = opt.TimestampGTE - } - if opt.TimestampLTE > 0 { - timeFilter["$lte"] = opt.TimestampLTE - } - filter[defaultTimestampField] = timeFilter - } - - cursor, err := r.db.Collection(auditLogCollection).Find(ctx, filter) - if err != nil { - return fmt.Errorf("find audit logs, err: %w", err) - } - - var result []*domain.AuditLog - if err := cursor.All(ctx, &result); err != nil { - return fmt.Errorf("decode audit logs, err: %w", err) - } - opt.Result = result - return nil -} diff --git a/manager/repository/repo_rbac.go b/manager/repository/repo_rbac.go new file mode 100644 index 0000000..7222606 --- /dev/null +++ b/manager/repository/repo_rbac.go @@ -0,0 +1,271 @@ +package repository + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/Gthulhu/api/manager/domain" + + "go.mongodb.org/mongo-driver/v2/bson" +) + +func (r *repo) CreateUser(ctx context.Context, user *domain.User) error { + if user == nil { + return errors.New("nil user") + } + + now := time.Now().UnixMilli() + if user.ID.IsZero() { + user.ID = bson.NewObjectID() + } + if user.CreatedTime == 0 { + user.CreatedTime = now + } + user.UpdatedTime = now + + res, err := r.db.Collection(userCollection).InsertOne(ctx, user) + if err != nil { + return fmt.Errorf("create user, err: %w", err) + } + if oid, ok := res.InsertedID.(bson.ObjectID); ok { + user.ID = oid + } + return nil +} + +func (r *repo) UpdateUser(ctx context.Context, user *domain.User) error { + if user == nil { + return errors.New("nil user") + } + if user.ID.IsZero() { + return errors.New("user id is required") + } + + user.UpdatedTime = time.Now().UnixMilli() + res, err := r.db.Collection(userCollection).ReplaceOne(ctx, bson.M{"_id": user.ID}, user) + if err != nil { + return fmt.Errorf("update user, err: %w", err) + } + if res.MatchedCount == 0 { + return domain.ErrNotFound + } + return nil +} + +func (r *repo) QueryUsers(ctx context.Context, opt *domain.QueryUserOptions) error { + if opt == nil { + return errors.New("nil query options") + } + + filter := bson.M{} + if len(opt.IDs) > 0 { + filter["_id"] = bson.M{"$in": opt.IDs} + } + if len(opt.UserNames) > 0 { + filter["username"] = bson.M{"$in": opt.UserNames} + } + + cursor, err := r.db.Collection(userCollection).Find(ctx, filter) + if err != nil { + return fmt.Errorf("find users, err: %w", err) + } + + var result []*domain.User + if err := cursor.All(ctx, &result); err != nil { + return fmt.Errorf("decode users, err: %w", err) + } + opt.Result = result + return nil +} + +func (r *repo) CreateRole(ctx context.Context, role *domain.Role) error { + if role == nil { + return errors.New("nil role") + } + + now := time.Now().UnixMilli() + if role.ID.IsZero() { + role.ID = bson.NewObjectID() + } + if role.CreatedTime == 0 { + role.CreatedTime = now + } + role.UpdatedTime = now + + res, err := r.db.Collection(roleCollection).InsertOne(ctx, role) + if err != nil { + return fmt.Errorf("create role, err: %w", err) + } + if oid, ok := res.InsertedID.(bson.ObjectID); ok { + role.ID = oid + } + return nil +} + +func (r *repo) UpdateRole(ctx context.Context, role *domain.Role) error { + if role == nil { + return errors.New("nil role") + } + if role.ID.IsZero() { + return errors.New("role id is required") + } + + role.UpdatedTime = time.Now().UnixMilli() + res, err := r.db.Collection(roleCollection).ReplaceOne(ctx, bson.M{"_id": role.ID}, role) + if err != nil { + return fmt.Errorf("update role, err: %w", err) + } + if res.MatchedCount == 0 { + return domain.ErrNotFound + } + return nil +} + +func (r *repo) QueryRoles(ctx context.Context, opt *domain.QueryRoleOptions) error { + if opt == nil { + return errors.New("nil query options") + } + + filter := bson.M{} + if len(opt.IDs) > 0 { + filter["_id"] = bson.M{"$in": opt.IDs} + } + if len(opt.Names) > 0 { + filter["name"] = bson.M{"$in": opt.Names} + } + + cursor, err := r.db.Collection(roleCollection).Find(ctx, filter) + if err != nil { + return fmt.Errorf("find roles, err: %w", err) + } + + var result []*domain.Role + if err := cursor.All(ctx, &result); err != nil { + return fmt.Errorf("decode roles, err: %w", err) + } + opt.Result = result + return nil +} + +func (r *repo) CreatePermission(ctx context.Context, permission *domain.Permission) error { + if permission == nil { + return errors.New("nil permission") + } + + if permission.ID.IsZero() { + permission.ID = bson.NewObjectID() + } + + res, err := r.db.Collection(permissionCollection).InsertOne(ctx, permission) + if err != nil { + return fmt.Errorf("create permission, err: %w", err) + } + if oid, ok := res.InsertedID.(bson.ObjectID); ok { + permission.ID = oid + } + return nil +} + +func (r *repo) UpdatePermission(ctx context.Context, permission *domain.Permission) error { + if permission == nil { + return errors.New("nil permission") + } + if permission.ID.IsZero() { + return errors.New("permission id is required") + } + + res, err := r.db.Collection(permissionCollection).ReplaceOne(ctx, bson.M{"_id": permission.ID}, permission) + if err != nil { + return fmt.Errorf("update permission, err: %w", err) + } + if res.MatchedCount == 0 { + return domain.ErrNotFound + } + return nil +} + +func (r *repo) QueryPermissions(ctx context.Context, opt *domain.QueryPermissionOptions) error { + if opt == nil { + return errors.New("nil query options") + } + + filter := bson.M{} + if len(opt.IDs) > 0 { + filter["_id"] = bson.M{"$in": opt.IDs} + } + if len(opt.Keys) > 0 { + filter["key"] = bson.M{"$in": opt.Keys} + } + if len(opt.Resources) > 0 { + filter["resource"] = bson.M{"$in": opt.Resources} + } + + cursor, err := r.db.Collection(permissionCollection).Find(ctx, filter) + if err != nil { + return fmt.Errorf("find permissions, err: %w", err) + } + + var result []*domain.Permission + if err := cursor.All(ctx, &result); err != nil { + return fmt.Errorf("decode permissions, err: %w", err) + } + opt.Result = result + return nil +} + +func (r *repo) CreateAuditLog(ctx context.Context, log *domain.AuditLog) error { + if log == nil { + return errors.New("nil audit log") + } + if log.ID.IsZero() { + log.ID = bson.NewObjectID() + } + if log.Timestamp == 0 { + log.Timestamp = time.Now().UnixMilli() + } + + res, err := r.db.Collection(auditLogCollection).InsertOne(ctx, log) + if err != nil { + return fmt.Errorf("create audit log, err: %w", err) + } + if oid, ok := res.InsertedID.(bson.ObjectID); ok { + log.ID = oid + } + return nil +} + +func (r *repo) QueryAuditLogs(ctx context.Context, opt *domain.QueryAuditLogOptions) error { + if opt == nil { + return errors.New("nil query options") + } + + filter := bson.M{} + if len(opt.UserIDs) > 0 { + filter["user_id"] = bson.M{"$in": opt.UserIDs} + } + + if opt.TimestampGTE > 0 || opt.TimestampLTE > 0 { + timeFilter := bson.M{} + if opt.TimestampGTE > 0 { + timeFilter["$gte"] = opt.TimestampGTE + } + if opt.TimestampLTE > 0 { + timeFilter["$lte"] = opt.TimestampLTE + } + filter[defaultTimestampField] = timeFilter + } + + cursor, err := r.db.Collection(auditLogCollection).Find(ctx, filter) + if err != nil { + return fmt.Errorf("find audit logs, err: %w", err) + } + + var result []*domain.AuditLog + if err := cursor.All(ctx, &result); err != nil { + return fmt.Errorf("decode audit logs, err: %w", err) + } + opt.Result = result + return nil +} diff --git a/manager/repository/strategy_repo.go b/manager/repository/strategy_repo.go new file mode 100644 index 0000000..1bbd768 --- /dev/null +++ b/manager/repository/strategy_repo.go @@ -0,0 +1,128 @@ +package repository + +import ( + "context" + "errors" + "time" + + "github.com/Gthulhu/api/manager/domain" + "go.mongodb.org/mongo-driver/v2/bson" +) + +func (r *repo) InsertStrategyAndIntents(ctx context.Context, strategy *domain.ScheduleStrategy, intents []*domain.ScheduleIntent) error { + if strategy == nil { + return errors.New("nil strategy") + } + if intents == nil { + return errors.New("nil intents") + } + now := time.Now().UnixMilli() + if strategy.CreatedTime == 0 { + strategy.CreatedTime = now + } + strategy.UpdatedTime = now + res, err := r.db.Collection(scheduleStrategyCollection).InsertOne(ctx, strategy) + if err != nil { + return err + } + if oid, ok := res.InsertedID.(bson.ObjectID); ok { + strategy.ID = oid + } + + for _, intent := range intents { + if intent.ID.IsZero() { + intent.ID = bson.NewObjectID() + } + intent.StrategyID = strategy.ID + if intent.CreatedTime == 0 { + intent.CreatedTime = now + } + if intent.UpdatedTime == 0 { + intent.UpdatedTime = now + } + } + _, err = r.db.Collection(scheduleIntentCollection).InsertMany(ctx, intents) + if err != nil { + return err + } + return nil +} + +func (r *repo) BatchUpdateIntentsState(ctx context.Context, intentIDs []bson.ObjectID, newState domain.IntentState) error { + update := bson.M{ + "$set": bson.M{ + "state": newState, + "updateTime": time.Now().UnixMilli(), + }, + } + _, err := r.db.Collection(scheduleIntentCollection).UpdateMany(ctx, bson.M{ + "_id": bson.M{"$in": intentIDs}, + }, update) + if err != nil { + return err + } + return nil +} + +func (r *repo) QueryStrategies(ctx context.Context, opt *domain.QueryStrategyOptions) error { + if opt == nil { + return errors.New("nil query options") + } + filter := bson.M{} + if len(opt.IDs) > 0 { + filter["_id"] = bson.M{"$in": opt.IDs} + } + if len(opt.K8SNamespaces) > 0 { + filter["k8sNamespace"] = bson.M{"$in": opt.K8SNamespaces} + } + cursor, err := r.db.Collection(scheduleStrategyCollection).Find(ctx, filter) + if err != nil { + return err + } + defer cursor.Close(ctx) + + for cursor.Next(ctx) { + var strategy domain.ScheduleStrategy + if err := cursor.Decode(&strategy); err != nil { + return err + } + opt.Result = append(opt.Result, &strategy) + } + return cursor.Err() +} + +func (r *repo) QueryIntents(ctx context.Context, opt *domain.QueryIntentOptions) error { + if opt == nil { + return errors.New("nil query options") + } + filter := bson.M{} + if len(opt.IDs) > 0 { + filter["_id"] = bson.M{"$in": opt.IDs} + } + if len(opt.K8SNamespaces) > 0 { + filter["k8sNamespace"] = bson.M{"$in": opt.K8SNamespaces} + } + if len(opt.StrategyIDs) > 0 { + filter["strategyID"] = bson.M{"$in": opt.StrategyIDs} + } + if len(opt.PodIDs) > 0 { + filter["podID"] = bson.M{"$in": opt.PodIDs} + } + if len(opt.States) > 0 { + filter["state"] = bson.M{"$in": opt.States} + } + cursor, err := r.db.Collection(scheduleIntentCollection).Find(ctx, filter) + if err != nil { + return err + } + defer cursor.Close(ctx) + + for cursor.Next(ctx) { + var intent domain.ScheduleIntent + if err := cursor.Decode(&intent); err != nil { + return err + } + opt.Result = append(opt.Result, &intent) + } + return cursor.Err() +} diff --git a/manager/rest/handler_test.go b/manager/rest/handler_test.go index e2282fc..2331ac9 100644 --- a/manager/rest/handler_test.go +++ b/manager/rest/handler_test.go @@ -9,10 +9,12 @@ import ( "io" "net/http" "net/http/httptest" + "os" "testing" "github.com/Gthulhu/api/config" "github.com/Gthulhu/api/manager/app" + "github.com/Gthulhu/api/manager/domain" "github.com/Gthulhu/api/manager/migration" "github.com/Gthulhu/api/manager/rest" "github.com/Gthulhu/api/pkg/container" @@ -37,6 +39,9 @@ type HandlerTestSuite struct { *container.ContainerBuilder mongoDBClient *mongo.Client mongoDBCfg config.MongoDBConfig + + MockK8SAdapter *domain.MockK8SAdapter + MockDMAdapter *domain.MockDecisionMakerAdapter } func (suite *HandlerTestSuite) SetupSuite() { @@ -46,6 +51,9 @@ func (suite *HandlerTestSuite) SetupSuite() { suite.Require().NoError(err, "Failed to create container builder") suite.ContainerBuilder = containerBuilder + suite.MockK8SAdapter = domain.NewMockK8SAdapter(suite.T()) + suite.MockDMAdapter = domain.NewMockDecisionMakerAdapter(suite.T()) + cfg, err := config.InitManagerConfig("manager_config.test.toml", config.GetAbsPath("config")) suite.Require().NoError(err, "Failed to initialize manager config") @@ -58,6 +66,12 @@ func (suite *HandlerTestSuite) SetupSuite() { handlerModule, err := app.HandlerModule(serviceModule) suite.Require().NoError(err, "Failed to create handler module") opt := fx.Options( + fx.Provide(func() domain.K8SAdapter { + return suite.MockK8SAdapter + }), + fx.Provide(func() domain.DecisionMakerAdapter { + return suite.MockDMAdapter + }), handlerModule, fx.Invoke(migration.RunMongoMigration), fx.Populate(&suite.Handler), @@ -87,6 +101,9 @@ func (suite *HandlerTestSuite) SetupTest() { } func (suite *HandlerTestSuite) TearDownSuite() { + if os.Getenv("LOCAL_TEST") == "true" { + return + } err := suite.ContainerBuilder.PruneAll() suite.Require().NoError(err, "Failed to terminate containers") } diff --git a/manager/rest/routes.go b/manager/rest/routes.go index 5b38e05..aa96eee 100644 --- a/manager/rest/routes.go +++ b/manager/rest/routes.go @@ -36,6 +36,11 @@ func (h *Handler) SetupRoutes(engine *echo.Echo) { apiV1.DELETE("/roles", h.echoHandler(h.DeleteRole), echo.WrapMiddleware(h.GetAuthMiddleware(domain.RoleDelete))) apiV1.GET("/roles", h.echoHandler(h.ListRoles), echo.WrapMiddleware(h.GetAuthMiddleware(domain.RoleRead))) apiV1.GET("/permissions", h.echoHandler(h.ListPermissions), echo.WrapMiddleware(h.GetAuthMiddleware(domain.PermissionRead))) + + // strategy routes + apiV1.POST("/strategies", h.echoHandler(h.CreateScheduleStrategy), echo.WrapMiddleware(h.GetAuthMiddleware(domain.ScheduleStrategyCreate))) + apiV1.GET("/strategies/self", h.echoHandler(h.ListSelfScheduleStrategies), echo.WrapMiddleware(h.GetAuthMiddleware(domain.ScheduleStrategyRead))) + apiV1.GET("/intents/self", h.echoHandler(h.ListSelfScheduleIntents), echo.WrapMiddleware(h.GetAuthMiddleware(domain.ScheduleIntentRead))) } } diff --git a/manager/rest/strategy_hdl.go b/manager/rest/strategy_hdl.go new file mode 100644 index 0000000..ab02465 --- /dev/null +++ b/manager/rest/strategy_hdl.go @@ -0,0 +1,197 @@ +package rest + +import ( + "net/http" + + "github.com/Gthulhu/api/manager/domain" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type LabelSelector struct { + Key string `json:"key,omitempty"` + Value string `json:"value,omitempty"` +} + +type CreateScheduleStrategyRequest struct { + StrategyNamespace string `json:"strategyNamespace,omitempty"` + LabelSelectors []LabelSelector `json:"labelSelectors,omitempty"` + K8sNamespace []string `json:"k8sNamespace,omitempty"` + CommandRegex string `json:"commandRegex,omitempty"` + Priority int `json:"priority,omitempty"` + ExecutionTime int64 `json:"executionTime,omitempty"` +} + +func (h *Handler) CreateScheduleStrategy(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var req CreateScheduleStrategyRequest + err := h.JSONBind(r, &req) + if err != nil { + h.ErrorResponse(ctx, w, http.StatusBadRequest, "Invalid request body", err) + return + } + + strategy := &domain.ScheduleStrategy{ + StrategyNamespace: req.StrategyNamespace, + LabelSelectors: make([]domain.LabelSelector, len(req.LabelSelectors)), + K8sNamespace: req.K8sNamespace, + CommandRegex: req.CommandRegex, + Priority: req.Priority, + ExecutionTime: req.ExecutionTime, + } + for i, ls := range req.LabelSelectors { + strategy.LabelSelectors[i] = domain.LabelSelector{ + Key: ls.Key, + Value: ls.Value, + } + } + + claims, ok := h.GetClaimsFromContext(ctx) + if !ok { + h.ErrorResponse(ctx, w, http.StatusUnauthorized, "Unauthorized", nil) + return + } + + err = h.Svc.CreateScheduleStrategy(ctx, &claims, strategy) + if err != nil { + h.HandleError(ctx, w, err) + return + } + + response := NewSuccessResponse[string](nil) + h.JSONResponse(ctx, w, http.StatusOK, response) +} + +type ListSchedulerStrategiesResponse struct { + Strategies []*ScheduleStrategy `json:"strategies"` +} + +type ScheduleStrategy struct { + ID bson.ObjectID `bson:"_id,omitempty"` + StrategyNamespace string `bson:"strategyNamespace,omitempty"` + LabelSelectors []LabelSelector `bson:"labelSelectors,omitempty"` + K8sNamespace []string `bson:"k8sNamespace,omitempty"` + CommandRegex string `bson:"commandRegex,omitempty"` + Priority int `bson:"priority,omitempty"` + ExecutionTime int64 `bson:"executionTime,omitempty"` +} + +func (h *Handler) ListSelfScheduleStrategies(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + claims, ok := h.GetClaimsFromContext(ctx) + if !ok { + h.ErrorResponse(ctx, w, http.StatusUnauthorized, "Unauthorized", nil) + return + } + + uid, err := claims.GetBsonObjectUID() + if err != nil { + h.ErrorResponse(ctx, w, http.StatusBadRequest, "Invalid user ID in token", err) + return + } + queryOpt := &domain.QueryStrategyOptions{ + CreatorIDs: []bson.ObjectID{uid}, + } + + err = h.Svc.ListScheduleStrategies(ctx, queryOpt) + if err != nil { + h.HandleError(ctx, w, err) + return + } + + resp := ListSchedulerStrategiesResponse{ + Strategies: make([]*ScheduleStrategy, len(queryOpt.Result)), + } + for i, ds := range queryOpt.Result { + resp.Strategies[i] = h.convertDomainStrategyToResponseStrategy(ds) + } + response := NewSuccessResponse[ListSchedulerStrategiesResponse](&resp) + h.JSONResponse(ctx, w, http.StatusOK, response) +} + +func (h *Handler) convertDomainStrategyToResponseStrategy(domainStrategy *domain.ScheduleStrategy) *ScheduleStrategy { + return &ScheduleStrategy{ + ID: domainStrategy.ID, + StrategyNamespace: domainStrategy.StrategyNamespace, + LabelSelectors: convertDomainLabelSelectorsToResponseLabelSelectors(domainStrategy.LabelSelectors), + K8sNamespace: domainStrategy.K8sNamespace, + CommandRegex: domainStrategy.CommandRegex, + Priority: domainStrategy.Priority, + ExecutionTime: domainStrategy.ExecutionTime, + } +} + +func convertDomainLabelSelectorsToResponseLabelSelectors(domainLabelSelectors []domain.LabelSelector) []LabelSelector { + responseLabelSelectors := make([]LabelSelector, len(domainLabelSelectors)) + for i, dls := range domainLabelSelectors { + responseLabelSelectors[i] = LabelSelector{ + Key: dls.Key, + Value: dls.Value, + } + } + return responseLabelSelectors +} + +type ListScheduleIntentsResponse struct { + Intents []*ScheduleIntent `json:"intents"` +} + +type ScheduleIntent struct { + ID bson.ObjectID `bson:"_id,omitempty"` + StrategyID bson.ObjectID `bson:"strategyID,omitempty"` + PodID string `bson:"podID,omitempty"` + NodeID string `bson:"nodeID,omitempty"` + K8sNamespace string `bson:"k8sNamespace,omitempty"` + CommandRegex string `bson:"commandRegex,omitempty"` + Priority int `bson:"priority,omitempty"` + ExecutionTime int64 `bson:"executionTime,omitempty"` + PodLabels map[string]string `bson:"podLabels,omitempty"` + State domain.IntentState `bson:"state,omitempty"` +} + +func (h *Handler) ListSelfScheduleIntents(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + claims, ok := h.GetClaimsFromContext(ctx) + if !ok { + h.ErrorResponse(ctx, w, http.StatusUnauthorized, "Unauthorized", nil) + return + } + + uid, err := claims.GetBsonObjectUID() + if err != nil { + h.ErrorResponse(ctx, w, http.StatusBadRequest, "Invalid user ID in token", err) + return + } + queryOpt := &domain.QueryIntentOptions{ + CreatorIDs: []bson.ObjectID{uid}, + } + + err = h.Svc.ListScheduleIntents(ctx, queryOpt) + if err != nil { + h.HandleError(ctx, w, err) + return + } + + resp := ListScheduleIntentsResponse{ + Intents: make([]*ScheduleIntent, len(queryOpt.Result)), + } + for i, di := range queryOpt.Result { + resp.Intents[i] = h.convertDomainIntentToResponseIntent(di) + } + response := NewSuccessResponse[ListScheduleIntentsResponse](&resp) + h.JSONResponse(ctx, w, http.StatusOK, response) +} + +func (h *Handler) convertDomainIntentToResponseIntent(domainIntent *domain.ScheduleIntent) *ScheduleIntent { + return &ScheduleIntent{ + ID: domainIntent.ID, + StrategyID: domainIntent.StrategyID, + PodID: domainIntent.PodID, + NodeID: domainIntent.NodeID, + K8sNamespace: domainIntent.K8sNamespace, + CommandRegex: domainIntent.CommandRegex, + Priority: domainIntent.Priority, + ExecutionTime: domainIntent.ExecutionTime, + PodLabels: domainIntent.PodLabels, + State: domainIntent.State, + } +} diff --git a/manager/rest/strategy_hdl_test.go b/manager/rest/strategy_hdl_test.go new file mode 100644 index 0000000..1d9aa5b --- /dev/null +++ b/manager/rest/strategy_hdl_test.go @@ -0,0 +1,65 @@ +package rest_test + +import ( + "net/http" + + "github.com/Gthulhu/api/config" + "github.com/Gthulhu/api/manager/domain" + "github.com/Gthulhu/api/manager/rest" + "github.com/stretchr/testify/mock" +) + +func (suite *HandlerTestSuite) TestIntegrationStrategyHandler() { + adminUser, adminPwd := config.GetConfig().Account.AdminEmail, config.GetConfig().Account.AdminPassword + adminToken := suite.login(adminUser, adminPwd.Value(), http.StatusOK) + + strategyReq := rest.CreateScheduleStrategyRequest{ + LabelSelectors: []rest.LabelSelector{ + { + Key: "test", Value: "test", + }, + }, + Priority: 100, + ExecutionTime: 100, + } + + suite.MockK8SAdapter.EXPECT().QueryPods(mock.Anything, mock.Anything).Return([]*domain.Pod{{PodID: "Test", Labels: map[string]string{"test": "test"}, NodeID: "test"}}, nil).Once() + suite.MockK8SAdapter.EXPECT().QueryDecisionMakerPods(mock.Anything, mock.Anything).Return([]*domain.DecisionMakerPod{{Host: "dm-host", NodeID: "test", Port: 8080}}, nil).Once() + suite.MockDMAdapter.EXPECT().SendSchedulingIntent(mock.Anything, mock.Anything, mock.Anything).Return(nil).Times(1) + suite.createStrategy(adminToken, &strategyReq, http.StatusOK) + + strategies := suite.listSelfStrategies(adminToken, http.StatusOK) + suite.Require().Len(strategies.Strategies, 1, "Expected one strategy") + suite.Require().Equal(strategyReq.LabelSelectors[0].Key, strategies.Strategies[0].LabelSelectors[0].Key, "Label selector key mismatch") + suite.Require().Equal(strategyReq.LabelSelectors[0].Value, strategies.Strategies[0].LabelSelectors[0].Value, "Label selector value mismatch") + suite.Require().Equal(strategyReq.Priority, strategies.Strategies[0].Priority, "Priority mismatch") + suite.Require().Equal(strategyReq.ExecutionTime, strategies.Strategies[0].ExecutionTime, "ExecutionTime mismatch") + + intents := suite.listSelfIntents(adminToken, http.StatusOK) + suite.Require().Len(intents.Intents, 1, "Expected one intent") + suite.Require().Equal("Test", intents.Intents[0].PodID, "PodID mismatch") + suite.Require().Equal(strategies.Strategies[0].ID.String(), intents.Intents[0].StrategyID.String(), "StrategyID mismatch") + suite.Require().Equal(domain.IntentStateSent, intents.Intents[0].State, "State mismatch") + suite.Require().Equal(strategyReq.Priority, intents.Intents[0].Priority, "Priority mismatch") + suite.Require().Equal(strategyReq.ExecutionTime, intents.Intents[0].ExecutionTime, "ExecutionTime mismatch") +} + +func (suite *HandlerTestSuite) createStrategy(token string, strategyReq *rest.CreateScheduleStrategyRequest, expectedStatus int) { + createStrategyResp := rest.SuccessResponse[string]{} + _, resp := suite.sendV1Request("POST", "/strategies", strategyReq, &createStrategyResp, token) + suite.Require().Equal(expectedStatus, resp.Code, "Unexpected status code on create strategy") +} + +func (suite *HandlerTestSuite) listSelfStrategies(token string, expectedStatus int) *rest.ListSchedulerStrategiesResponse { + listStrategiesResp := rest.SuccessResponse[rest.ListSchedulerStrategiesResponse]{} + _, resp := suite.sendV1Request("GET", "/strategies/self", nil, &listStrategiesResp, token) + suite.Require().Equal(expectedStatus, resp.Code, "Unexpected status code on create strategy") + return listStrategiesResp.Data +} + +func (suite *HandlerTestSuite) listSelfIntents(token string, expectedStatus int) *rest.ListScheduleIntentsResponse { + listStrategiesResp := rest.SuccessResponse[rest.ListScheduleIntentsResponse]{} + _, resp := suite.sendV1Request("GET", "/intents/self", nil, &listStrategiesResp, token) + suite.Require().Equal(expectedStatus, resp.Code, "Unexpected status code on create strategy") + return listStrategiesResp.Data +} diff --git a/manager/service/strategy_svc.go b/manager/service/strategy_svc.go new file mode 100644 index 0000000..efa4f87 --- /dev/null +++ b/manager/service/strategy_svc.go @@ -0,0 +1,107 @@ +package service + +import ( + "context" + "fmt" + "net/http" + + "github.com/Gthulhu/api/manager/domain" + "github.com/Gthulhu/api/manager/errs" + "github.com/Gthulhu/api/pkg/logger" + "github.com/pkg/errors" + "go.mongodb.org/mongo-driver/v2/bson" +) + +func (svc *Service) CreateScheduleStrategy(ctx context.Context, operator *domain.Claims, strategy *domain.ScheduleStrategy) error { + operatorID, err := operator.GetBsonObjectUID() + if err != nil { + return errors.WithMessagef(err, "invalid operator ID %s", operator.UID) + } + queryOpt := &domain.QueryPodsOptions{ + K8SNamespace: strategy.K8sNamespace, + LabelSelectors: strategy.LabelSelectors, + CommandRegex: strategy.CommandRegex, + } + pods, err := svc.K8SAdapter.QueryPods(ctx, queryOpt) + if err != nil { + return err + } + if len(pods) == 0 { + return errs.NewHTTPStatusError(http.StatusNotFound, "no pods match the strategy criteria", fmt.Errorf("no pods found for the given namespaces and label selectors, opts:%+v", queryOpt)) + } + + logger.Logger(ctx).Debug().Msgf("found %d pods matching the strategy criteria", len(pods)) + + strategy.BaseEntity = domain.NewBaseEntity(&operatorID, &operatorID) + + intents := make([]*domain.ScheduleIntent, 0, len(pods)) + nodeIDsMap := make(map[string]struct{}) + nodeIDs := make([]string, 0) + for _, pod := range pods { + intent := domain.NewScheduleIntent(strategy, pod) + intents = append(intents, &intent) + if _, exists := nodeIDsMap[pod.NodeID]; !exists { + nodeIDsMap[pod.NodeID] = struct{}{} + nodeIDs = append(nodeIDs, pod.NodeID) + } + } + + err = svc.Repo.InsertStrategyAndIntents(ctx, strategy, intents) + if err != nil { + return fmt.Errorf("insert strategy and intents into repository: %w", err) + } + + dmLabel := domain.LabelSelector{ + Key: "role", + Value: "decision-maker", + } + + dmQueryOpt := &domain.QueryDecisionMakerPodsOptions{ + DecisionMakerLabel: dmLabel, + NodeIDs: nodeIDs, + } + dms, err := svc.K8SAdapter.QueryDecisionMakerPods(ctx, dmQueryOpt) + if err != nil { + return err + } + if len(dms) == 0 { + logger.Logger(ctx).Warn().Msgf("no decision maker pods found for scheduling intents, opts:%+v", dmQueryOpt) + return nil + } + + logger.Logger(ctx).Debug().Msgf("found %d decision maker pods for scheduling intents", len(dms)) + + nodeIDIntentsMap := make(map[string][]*domain.ScheduleIntent) + nodeIDIntentIDsMap := make(map[string][]bson.ObjectID) + nodeIDDMap := make(map[string]*domain.DecisionMakerPod) + for _, dmPod := range dms { + for _, intent := range intents { + if intent.NodeID == dmPod.NodeID { + nodeIDIntentIDsMap[dmPod.Host] = append(nodeIDIntentIDsMap[dmPod.Host], intent.ID) + nodeIDIntentsMap[dmPod.Host] = append(nodeIDIntentsMap[dmPod.Host], intent) + nodeIDDMap[dmPod.Host] = dmPod + } + } + } + for host, intents := range nodeIDIntentsMap { + dmPod := nodeIDDMap[host] + err = svc.DMAdapter.SendSchedulingIntent(ctx, dmPod, intents) + if err != nil { + return fmt.Errorf("send scheduling intents to decision maker %s: %w", host, err) + } + err = svc.Repo.BatchUpdateIntentsState(ctx, nodeIDIntentIDsMap[host], domain.IntentStateSent) + if err != nil { + return fmt.Errorf("insert strategy and intents into repository: %w", err) + } + logger.Logger(ctx).Info().Msgf("sent %d scheduling intents to decision maker %s", len(intents), host) + } + return nil +} + +func (svc *Service) ListScheduleStrategies(ctx context.Context, filterOpts *domain.QueryStrategyOptions) error { + return svc.Repo.QueryStrategies(ctx, filterOpts) +} + +func (svc *Service) ListScheduleIntents(ctx context.Context, filterOpts *domain.QueryIntentOptions) error { + return svc.Repo.QueryIntents(ctx, filterOpts) +} diff --git a/manager/service/svc.go b/manager/service/svc.go index 908e7fc..5753016 100644 --- a/manager/service/svc.go +++ b/manager/service/svc.go @@ -19,6 +19,8 @@ type Params struct { Repo domain.Repository KeyConfig config.KeyConfig AccountConfig config.AccountConfig + K8SAdapter domain.K8SAdapter + DMAdapter domain.DecisionMakerAdapter } func NewService(params Params) (domain.Service, error) { @@ -28,6 +30,8 @@ func NewService(params Params) (domain.Service, error) { } svc := &Service{ + K8SAdapter: params.K8SAdapter, + DMAdapter: params.DMAdapter, Repo: params.Repo, jwtPrivateKey: jwtPrivateKey, } @@ -43,6 +47,8 @@ func NewService(params Params) (domain.Service, error) { } type Service struct { + K8SAdapter domain.K8SAdapter + DMAdapter domain.DecisionMakerAdapter Repo domain.Repository jwtPrivateKey *rsa.PrivateKey } From 73539516e7d1a6001fde57afbfacfcf558a71443 Mon Sep 17 00:00:00 2001 From: Vic Xu Date: Sat, 13 Dec 2025 23:13:43 +0800 Subject: [PATCH 16/18] add kind init script to test in k8s cluster --- .gitignore | 1 + Dockerfile => Dockerfile.amd64 | 16 +- Makefile | 21 +- config/dm_config.default.toml | 7 + config/dm_config.go | 47 ++ config/manager_config.default.toml | 58 +- config/manager_config.go | 17 +- config/manager_config.test.toml | 2 +- decisionmaker/app/module.go | 36 + decisionmaker/app/rest_app.go | 62 ++ decisionmaker/client/.keep | 0 decisionmaker/cmd/.keep | 0 decisionmaker/cmd/cmd.go | 73 ++ decisionmaker/domain/.keep | 0 decisionmaker/domain/pod.go | 27 + decisionmaker/rest/.keep | 0 decisionmaker/rest/handler.go | 205 ++++++ decisionmaker/service/.keep | 0 decisionmaker/service/service.go | 178 +++++ decisionmaker/service/service_test.go | 87 +++ deployment/kind/decisonmaker/daemonset.yaml | 104 +++ deployment/kind/decisonmaker/service.yaml | 14 + deployment/kind/local_setup.sh | 82 +++ deployment/kind/local_teardown.sh | 15 + deployment/kind/manager/deployment.yaml | 100 +++ deployment/kind/manager/service.yaml | 14 + deployment/kind/mongo/secret.yaml | 34 + deployment/kind/mongo/service.yaml | 14 + deployment/kind/mongo/statefulset.yaml | 187 ++++++ deployment/kind/pod/busybox.yaml | 15 + deployment/local/docker-compose.infra.yaml | 46 +- deployment/local/mongo-keyfile | 16 + docs/{ => manager}/docs.go | 701 +++++++++++++++----- docs/{ => manager}/swagger.json | 693 +++++++++++++++---- docs/{ => manager}/swagger.yaml | 506 ++++++++++---- main.go | 50 +- manager/app/module.go | 3 +- manager/app/rest_app.go | 36 +- manager/client/deicison_maker.go | 54 +- manager/cmd/cmd.go | 73 ++ manager/domain/k8s_resource.go | 5 + manager/domain/strategy.go | 2 + manager/k8s_adapter/adapter.go | 6 + manager/k8s_adapter/adapter_local_test.go | 3 + manager/k8s_adapter/adapter_test.go | 3 + manager/migration/tool.go | 10 +- manager/repository/repo.go | 6 +- manager/rest/auth_hdl_test.go | 2 +- manager/rest/handler_test.go | 17 +- manager/rest/role_hdl_test.go | 2 +- manager/rest/routes.go | 2 +- manager/rest/strategy_hdl.go | 40 ++ manager/rest/strategy_hdl_test.go | 2 +- manager/service/strategy_svc.go | 4 +- pkg/middleware/logger.go | 79 +++ 55 files changed, 3230 insertions(+), 547 deletions(-) rename Dockerfile => Dockerfile.amd64 (67%) create mode 100644 config/dm_config.default.toml create mode 100644 config/dm_config.go create mode 100644 decisionmaker/app/module.go create mode 100644 decisionmaker/app/rest_app.go delete mode 100644 decisionmaker/client/.keep delete mode 100644 decisionmaker/cmd/.keep create mode 100644 decisionmaker/cmd/cmd.go delete mode 100644 decisionmaker/domain/.keep create mode 100644 decisionmaker/domain/pod.go delete mode 100644 decisionmaker/rest/.keep create mode 100644 decisionmaker/rest/handler.go delete mode 100644 decisionmaker/service/.keep create mode 100644 decisionmaker/service/service.go create mode 100644 decisionmaker/service/service_test.go create mode 100644 deployment/kind/decisonmaker/daemonset.yaml create mode 100644 deployment/kind/decisonmaker/service.yaml create mode 100644 deployment/kind/local_setup.sh create mode 100644 deployment/kind/local_teardown.sh create mode 100644 deployment/kind/manager/deployment.yaml create mode 100644 deployment/kind/manager/service.yaml create mode 100644 deployment/kind/mongo/secret.yaml create mode 100644 deployment/kind/mongo/service.yaml create mode 100644 deployment/kind/mongo/statefulset.yaml create mode 100644 deployment/kind/pod/busybox.yaml create mode 100644 deployment/local/mongo-keyfile rename docs/{ => manager}/docs.go (60%) rename docs/{ => manager}/swagger.json (60%) rename docs/{ => manager}/swagger.yaml (51%) create mode 100644 manager/cmd/cmd.go create mode 100644 pkg/middleware/logger.go diff --git a/.gitignore b/.gitignore index 594a4ad..c3c8e88 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ bin api config/manager_config.toml +config/*_config.toml *.ref \ No newline at end of file diff --git a/Dockerfile b/Dockerfile.amd64 similarity index 67% rename from Dockerfile rename to Dockerfile.amd64 index 406b1c5..0afa22c 100644 --- a/Dockerfile +++ b/Dockerfile.amd64 @@ -1,5 +1,5 @@ # Build stage -FROM golang:1.24-alpine AS builder +FROM golang:1.24-alpine3.22 AS builder # Set working directory WORKDIR /app @@ -17,7 +17,7 @@ COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . # Final stage -FROM alpine:latest +FROM alpine:3.22 # Install ca-certificates for HTTPS and bash for scripts RUN apk --no-cache add ca-certificates bash curl @@ -27,16 +27,12 @@ WORKDIR /app/ # Copy the binary from builder stage COPY --from=builder /app/main . -COPY config ./ +COPY ./config/dm_config.default.toml /app/config/dm_config.toml +COPY ./config/manager_config.default.toml /app/config/manager_config.toml +COPY ./manager/migration /app/manager/migration # Create directory for Kubernetes config RUN mkdir -p /app/.kube # Expose port -EXPOSE 8080 - -# Set environment variables -ENV GIN_MODE=release - -# Run the application with in-cluster mode by default -ENTRYPOINT ["bash"] +CMD [ "/app/main", "manager" ] \ No newline at end of file diff --git a/Makefile b/Makefile index f854a67..0b32bcd 100644 --- a/Makefile +++ b/Makefile @@ -86,6 +86,10 @@ local-infra-up: @echo "Starting local infrastructure with Docker Compose..." docker-compose -f $(CURDIR)/deployment/local/docker-compose.infra.yaml up -d +local-infra-down: + @echo "Stopping local infrastructure with Docker Compose..." + docker-compose -f $(CURDIR)/deployment/local/docker-compose.infra.yaml down + local-run-manager: @echo "Running Manager locally..." go run main.go manager --config-dir $(CURDIR)/config/manager_config.toml --config-name manager_config @@ -95,4 +99,19 @@ local-run-manger-migration: migrate \ -path $(CURDIR)/manager/migration \ -database "mongodb://test:test@localhost:27017/manager?authSource=admin" \ - -verbose up \ No newline at end of file + -verbose up + + +local-build-image.amd64: + @echo "Building local Docker image for API Server..." + docker build -f Dockerfile.amd64 -t gthulhu-api:local . + +gen-manager-swagger: + @echo "Generating Swagger documentation for Manager..." + swag init -g ./manager/cmd/cmd.go -o docs/manager + +local-kind-setup: + sh $(CURDIR)/deployment/kind/local_setup.sh + +local-kind-teardown: + sh $(CURDIR)/deployment/kind/local_teardown.sh \ No newline at end of file diff --git a/config/dm_config.default.toml b/config/dm_config.default.toml new file mode 100644 index 0000000..322410d --- /dev/null +++ b/config/dm_config.default.toml @@ -0,0 +1,7 @@ +[server] +host = ":8080" + + +[logging] +level = "info" +path = "logs/app.log" diff --git a/config/dm_config.go b/config/dm_config.go new file mode 100644 index 0000000..af78f10 --- /dev/null +++ b/config/dm_config.go @@ -0,0 +1,47 @@ +package config + +import ( + "strings" + + "github.com/spf13/viper" +) + +type DecisionMakerConfig struct { + Server ServerConfig `mapstructure:"server"` + Logging LoggingConfig `mapstructure:"logging"` +} + +var ( + dmConfig *DecisionMakerConfig +) + +func GetDMConfig() *ManageConfig { + return managerCfg +} + +func InitDMConfig(configName string, configPath string) (DecisionMakerConfig, error) { + var cfg DecisionMakerConfig + if configPath != "" { + viper.AddConfigPath(configPath) + } + if configName == "" { + configName = "dm_config" + } + viper.AddConfigPath(GetAbsPath("config")) + viper.SetConfigName(configName) + viper.SetConfigType("toml") + viper.SetEnvPrefix("DM") + viper.AutomaticEnv() + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + err := viper.ReadInConfig() + if err != nil { + return cfg, err + } + + err = viper.Unmarshal(&cfg) + if err != nil { + return cfg, err + } + dmConfig = &cfg + return cfg, nil +} diff --git a/config/manager_config.default.toml b/config/manager_config.default.toml index 5e8659a..625840e 100644 --- a/config/manager_config.default.toml +++ b/config/manager_config.default.toml @@ -12,6 +12,8 @@ port = "27017" user = "test" password = "test" database = "manager" +options = "authSource=admin&directConnection=true&ssl=false" +ca_pem_enable = false ca_pem = """ -----BEGIN CERTIFICATE----- YOUR_CERTIFICATE_HERE @@ -21,16 +23,58 @@ YOUR_CERTIFICATE_HERE [key] rsa_private_key_pem = """ -----BEGIN RSA PRIVATE KEY----- -YOUR_PRIVATE_KEY_HERE +MIIJJwIBAAKCAgEAny28YMC2/+yYj3T29lz60uryNz8gNVrqD7lTJuHQ3DMTE6AD +qnERy8VgHve0tWzhJc5ZBZ1Hduvj+z/kNqbcU81YGhmfOrQ3iFNYBlSAseIHdAw3 +9HGyC6OKzTXI4HRpc8CwcF6hKExkyWlkALr5i+IQDfimvarjjZ6Nm368L0Rthv3K +OkI5CqRZ6bsVwwBug7GcdkvFs3LiRSKlMBpH2tCkZ5ZZE8VyuK7VnlwV7n6EHzN5 +BqaHq8HVLw2KzvibSi+/5wIZV2Yx33tViLbhOsZqLt6qQCGGgKzNX4TGwRLGAiVV +1NCpgQhimZ4YP2thqSsqbaISOuvFlYq+QGP1bcvcHB7UhT1ZnHSDYcbT2qiD3Voq +ytXVKLB1X5XCD99YLSP9B32f1lvZD4MhDtE4IhAuqn15MGB5ct4yj/uMldFScs9K +hqnWcwS4K6Qx3IfdB+ZxT5hEOWJLEcGqe/CSXITNG7oS9mrSAJJvHSLz++4R/Sh1 +MnT2YWjyDk6qeeqAwut0w5iDKWt7qsGEcHFPIVVlos+xLfrPDtgHQk8upjslUcMy +MDTf21Y3RdJ3k1gTR9KHEwzKeiNlLjen9ekFWupF8jik1aYRWL6h54ZyGxwKEyMY +i9o18G2pXPzvVaPYtU+TGXdO4QwiES72TNCDbNaGj75Gj0sN+LfjjQ4A898CAwEA +AQKCAgAFrHuqdzQOq0BE3MZwwZ+vJPC9R2K+hB8TsGdmW2Y2cxua93kp+h3IRaDH +eczXKqpbzp8dtB13/7CApCZeTFROKGObio5CaWoRUecxUpHDxWq+mDDmZacTAyFP +bztZxMx9c8DWQIk+BnsRMtB9tixu7//if5px6EV0JtKlWD8c8DN3PFSY/wNJfdI2 +opSD/t/xkcMh9FF3tACctj9tF4K4KfeyOYmzSrZsHs8+dcnSVnAfLJaDxivP03jl +1HW+Kt5eJpWQhmKg2uOsM5k45kvg7HGcehNXddp1e7NWVEVBXInySaJlk4p3LvVU +xG3Y1NsGTKOWhNBhiUXhrrBZWzbERLvE7/OtrHVDAlEuA47rFb/rTnMWHIZhNeXt +Hwa7G/11dlwYN3jn5u+/2SkVC0R4X/lqTzFRzCYIWr9lTeVpC53Gn1jmX2PSNDbF +yLi7ZZFBhS8GdNimeKyJReKV2o2nsO49KngGIs5/B318GA5BwBVPf9fVd9a2n29s +ioUXmTE07bpbyXk1BL4jZFOAJYqXh6IOwAFKFtQXgfidod7KcwpxQAJpE1Od3EPd +sGlTTC+hsUzAdlV82wqc6AB80DcxlzsI0adTkD7NrIcRSQtCC9DnZPXm/kFiam80 +gW2SmIsaLYauoQgIcbI1Lpy3rMCMbbTeG7K3KGyYZULFXHRsAQKCAQEAy5n5Xt8z +VaCS+V0VrwpIdPAOzgaxJkcfy3hVLe5vkhaeu5DzyHgW0fPud0P2J1eUD8GWxXvj +6EImFsFydH+DR8ClhHX8awpEn4mS6vR6VVPseqKWzs6BP7usH/WNTIeGJR2z0hRG +ZgD/z4W1PwwL6tIno/oHY3MkflX5/1X6q7vGjNjN7d81Mb48dXcLEjaZWtcRqzy0 +MNXrgvrpe/pCRUeUBV6WYHSPn6OPJA7RgcKegn9AwWEoecieFnjiVSjdfI2WT+Wa +13MCwPoWs1u+mHNl+yaqFnj9u9tF2xF6M8ZERscmvU+k0aDNcuJq/drP6qH/93gn +e7Tjwlf3IqhcLwKCAQEAyCUFaRimDfn43qLBDZpgmBHwCpBQFaNS8jvVQpUlQ4zL +W/pCqMIePajE5E9EBcQZyd+tE6WE202Z2CBzvfyH54HGmAiEE5koQSE6GBZbXrzu +4ToolPR6nrlEDR6ayjuv9BOPR91OZL9EfSgi5kdNIEilnDm1n/VhEIW5y6TsJUGv +VeuTDgnRYbkIVBBppA5U4rYyOz7ES+0L344k05i9LzFvcgc1QX50Dg8dqiI2tm6z +uyjxhJ9TW6R1iqzLnDB01YrDuWN62qISHnNz4X5Z/EBoITo3Og2IMaCeE5yKHCjw +FrrV05F8Cf7B5DLBOTWiX3oPtu9oV7QrjN6WARGHUQKCAQARrH8KLkPthe/cN6lf +NXxOslwGpGwST5BCAGMchpsmylHjJFUVLN+GQC+OKNcgWSjgKUTmRbfl/IAD76z4 +0ezaeK2ljvxnak/ErZOUU76e05cumhiPQTvVBXyOlak7YHRTmn12mg32YtXR9OBj +5a7PJokMYfLsPh2H3fzCnnsRF07IATX3FS4v8DydUcUjQpwTV6IQBEf8CUXVa+SC +v5mrG+iMgsZ4/wVMrU0Kq0KiiftqhpNfdgimcbTPbJTxIYgAfOX0b5D+bNxrVgpM +bYVhBHtwzs1q//u+p+0rdBvwjKB2qGkDe/tpuxS6iU8SVEFCM+fdWo/K3Ev9Hde1 +KXo/AoIBABUAdYHiuUIMMgZCs9lWkr5CW5rwK8cpfUG375fuCJv/ATPknewRepTj +yc1fV/b27fHWC9Zc7wUILpWUSjDsd+JeJtW7Rwi7cJLtBqiSaAIX90UhEjMXOGrB +bBeoV3vTKZKGHunemiROQcSUWp0pbDlwBhjPoXRojkfqkGWDJ9h8/QYaEzNM6nDD +ttEDa+JwMo4bqke3PWfuNum9g7XEeE2kdVpU0UzPFSSIh4db0bvw/+Eq2bUd9uRN +7JuhqDf6ibgCuKkSfEjG6vnRCZ7m4FBs/cBG2Ja55sm2XgAW1BNCZHcuIdPylz6B +Qh1NCiOTsjcsmsuKcbuKR2ufy8PO8BECggEACr4AaasrHcqqBcGCZfGlO4Q0Bbzu +1IJ6u9X+0U+k36wH9cE8RMJZaFo30bYTSBnxN6YY4M1taG3egIZQNgTRpjEiC8lV +spxkAVwlY6g1ZER7IFXlzhz6wYuDBayRhA/2zBPgtGesfpTd7H24AlJ6qB+mThHs +7ZbETM4KP04uNBBPybwaPIOkUPnQInfIPOAJVHebHp8atyavwrpGmRq958XKMgvz +8oHIdzV+XTMi1+U7eg/ITEpOAPD82fYB4UfKRdA1jraXsJJyTs+QFjc2WXBffgAg +X6m7Mp9nAMhRyXhULslO3trWFbFCa2dbQkDSyBRvsb2HZtztoLVyo1mtUg== -----END RSA PRIVATE KEY----- """ -rsa_public_key_pem = """ ------BEGIN PUBLIC KEY----- -YOUR_PUBLIC_KEY_HERE ------END PUBLIC KEY----- -""" - [account] admin_email = "admin@example.com" admin_password = "your-password-here" diff --git a/config/manager_config.go b/config/manager_config.go index 8b0120a..849c97a 100644 --- a/config/manager_config.go +++ b/config/manager_config.go @@ -39,13 +39,14 @@ type ManageConfig struct { } type MongoDBConfig struct { - Database string `mapstructure:"database"` - CAPem SecretValue `mapstructure:"ca_pem"` - User string `mapstructure:"user"` - Password SecretValue `mapstructure:"password"` - Port string `mapstructure:"port"` - Host string `mapstructure:"host"` - Options string `mapstructure:"options"` + Database string `mapstructure:"database"` + CAPem SecretValue `mapstructure:"ca_pem"` + CAPemEnable bool `mapstructure:"ca_pem_enable"` + User string `mapstructure:"user"` + Password SecretValue `mapstructure:"password"` + Port string `mapstructure:"port"` + Host string `mapstructure:"host"` + Options string `mapstructure:"options"` } func (mc MongoDBConfig) GetURI() string { @@ -70,7 +71,7 @@ var ( managerCfg *ManageConfig ) -func GetConfig() *ManageConfig { +func GetManagerConfig() *ManageConfig { return managerCfg } diff --git a/config/manager_config.test.toml b/config/manager_config.test.toml index 5cedb7a..4cb483d 100644 --- a/config/manager_config.test.toml +++ b/config/manager_config.test.toml @@ -13,7 +13,7 @@ port = "27017" user = "test" password = "test" database = "manager" -options = "authSource=admin&ssl=false" +options = "authSource=admin&directConnection=true&ssl=false" [key] rsa_private_key_pem = """ diff --git a/decisionmaker/app/module.go b/decisionmaker/app/module.go new file mode 100644 index 0000000..3de4872 --- /dev/null +++ b/decisionmaker/app/module.go @@ -0,0 +1,36 @@ +package app + +import ( + "github.com/Gthulhu/api/config" + "github.com/Gthulhu/api/decisionmaker/rest" + "github.com/Gthulhu/api/decisionmaker/service" + "go.uber.org/fx" +) + +// ConfigModule creates an Fx module that provides configuration structs +func ConfigModule(cfg config.DecisionMakerConfig) (fx.Option, error) { + return fx.Options( + fx.Provide(func() config.DecisionMakerConfig { + return cfg + }), + fx.Provide(func(dmCfg config.DecisionMakerConfig) config.ServerConfig { + return dmCfg.Server + }), + ), nil +} + +func ServiceModule() (fx.Option, error) { + return fx.Options( + fx.Provide(func() service.Service { + return service.NewService() + }), + ), nil +} + +// HandlerModule creates an Fx module that provides the REST handler, return *rest.Handler +func HandlerModule(opt fx.Option) (fx.Option, error) { + return fx.Options( + opt, + fx.Provide(rest.NewHandler), + ), nil +} diff --git a/decisionmaker/app/rest_app.go b/decisionmaker/app/rest_app.go new file mode 100644 index 0000000..da79637 --- /dev/null +++ b/decisionmaker/app/rest_app.go @@ -0,0 +1,62 @@ +package app + +import ( + "context" + + "github.com/Gthulhu/api/config" + "github.com/Gthulhu/api/decisionmaker/rest" + "github.com/Gthulhu/api/pkg/logger" + "github.com/labstack/echo/v4" + "go.uber.org/fx" +) + +func NewRestApp(configName string, configDirPath string) (*fx.App, error) { + cfg, err := config.InitDMConfig(configName, configDirPath) + if err != nil { + return nil, err + } + cfgModule, err := ConfigModule(cfg) + if err != nil { + return nil, err + } + svcModule, err := ServiceModule() + handlerModule, err := HandlerModule(fx.Options(cfgModule, svcModule)) + if err != nil { + return nil, err + } + + app := fx.New( + handlerModule, + fx.Invoke(StartRestApp), + ) + return app, nil +} + +func StartRestApp(lc fx.Lifecycle, cfg config.ServerConfig, handler *rest.Handler) error { + engine := echo.New() + handler.SetupRoutes(engine) + + // TODO: setup middleware, logging, etc. + + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + serverHost := cfg.Host + if serverHost == "" { + serverHost = ":8082" + } + go func() { + logger.Logger(ctx).Info().Msgf("starting dm server on port %s", serverHost) + if err := engine.Start(serverHost); err != nil { + logger.Logger(ctx).Fatal().Err(err).Msgf("start rest server fail on port %s", serverHost) + } + }() + return nil + }, + OnStop: func(ctx context.Context) error { + logger.Logger(ctx).Info().Msg("shutting down dm server") + return engine.Shutdown(ctx) + }, + }) + + return nil +} diff --git a/decisionmaker/client/.keep b/decisionmaker/client/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/decisionmaker/cmd/.keep b/decisionmaker/cmd/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/decisionmaker/cmd/cmd.go b/decisionmaker/cmd/cmd.go new file mode 100644 index 0000000..29499b1 --- /dev/null +++ b/decisionmaker/cmd/cmd.go @@ -0,0 +1,73 @@ +package cmd + +import ( + "context" + "os" + + dmapp "github.com/Gthulhu/api/decisionmaker/app" + "github.com/Gthulhu/api/pkg/logger" + "github.com/spf13/cobra" +) + +func init() { + DMCmd.Flags().StringP("config-name", "c", "", "Configuration file name without extension") + DMCmd.Flags().StringP("config-dir", "d", "", "Configuration file directory path") +} + +// @title manager service +// @version 1.0 +// @description manager service API documentation +// @termsOfService http://swagger.io/terms/ + +// @contact.name API Support + +// @host localhost:8081 +// @BasePath / + +// @Accept json +// @Produce json + +// @securityDefinitions.apikey BearerAuth +// @in header +// @name Authorization + +// @schemes http + +// @externalDocs.description OpenAPI +// @externalDocs.url https://swagger.io/resources/open-api/ +func RunManagerApp(cmd *cobra.Command, args []string) { + configName, configDirPath := getConfigInfo(cmd) + logger.InitLogger() + app, err := dmapp.NewRestApp(configName, configDirPath) + if err != nil { + logger.Logger(context.Background()).Fatal().Err(err).Msg("failed to create rest app") + } + app.Run() +} + +func getConfigInfo(cmd *cobra.Command) (string, string) { + configName := "dm_config" + configDirPath := "" + if cmd != nil { + configNameFlag, err := cmd.Flags().GetString("config-name") + if err == nil && configNameFlag != "" { + configName = configNameFlag + } + configPathFlag, err := cmd.Flags().GetString("config-dir") + if err == nil && configPathFlag != "" { + configDirPath = configPathFlag + } + } + if envConfigName := os.Getenv("DM_CONFIG_NAME"); envConfigName != "" { + configName = envConfigName + } + if envConfigPath := os.Getenv("DM_CONFIG_DIR_PATH"); envConfigPath != "" { + configDirPath = envConfigPath + } + return configName, configDirPath +} + +var DMCmd = &cobra.Command{ + Run: RunManagerApp, + Use: "decisionmaker", +} diff --git a/decisionmaker/domain/.keep b/decisionmaker/domain/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/decisionmaker/domain/pod.go b/decisionmaker/domain/pod.go new file mode 100644 index 0000000..19c3fc2 --- /dev/null +++ b/decisionmaker/domain/pod.go @@ -0,0 +1,27 @@ +package domain + +// PodProcess represents a process information within a pod +type PodProcess struct { + PID int `json:"pid"` + Command string `json:"command"` + PPID int `json:"ppid,omitempty"` + ContainerID string `json:"container_id,omitempty"` +} + +// PodInfo represents pod information with associated processes +type PodInfo struct { + PodUID string `json:"pod_uid"` + PodID string `json:"pod_id,omitempty"` + Processes []PodProcess `json:"processes"` +} + +type Intent struct { + PodName string `json:"podName,omitempty"` + PodID string `json:"podID,omitempty"` + NodeID string `json:"nodeID,omitempty"` + K8sNamespace string `json:"k8sNamespace,omitempty"` + CommandRegex string `json:"commandRegex,omitempty"` + Priority int `json:"priority,omitempty"` + ExecutionTime int64 `json:"executionTime,omitempty"` + PodLabels map[string]string `json:"podLabels,omitempty"` +} diff --git a/decisionmaker/rest/.keep b/decisionmaker/rest/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/decisionmaker/rest/handler.go b/decisionmaker/rest/handler.go new file mode 100644 index 0000000..b60a0c9 --- /dev/null +++ b/decisionmaker/rest/handler.go @@ -0,0 +1,205 @@ +package rest + +import ( + "context" + "encoding/json" + "net/http" + "time" + + "github.com/Gthulhu/api/decisionmaker/domain" + "github.com/Gthulhu/api/decisionmaker/service" + "github.com/Gthulhu/api/manager/errs" + "github.com/Gthulhu/api/pkg/logger" + "github.com/Gthulhu/api/pkg/middleware" + "github.com/labstack/echo/v4" + "go.uber.org/fx" +) + +// ErrorResponse represents error response structure +type ErrorResponse struct { + Success bool `json:"success"` + Error string `json:"error"` +} + +// EmptyResponse is used for endpoints that return no data payload. +type EmptyResponse struct{} + +// VersionResponse describes the version endpoint payload. +type VersionResponse struct { + Message string `json:"message"` + Version string `json:"version"` + Endpoints string `json:"endpoints"` +} + +// HealthResponse describes the health check payload. +type HealthResponse struct { + Status string `json:"status"` + Timestamp string `json:"timestamp"` + Service string `json:"service"` +} + +func NewSuccessResponse[T any](data *T) SuccessResponse[T] { + return SuccessResponse[T]{ + Success: true, + Data: data, + Timestamp: time.Now().UTC().Format(time.RFC3339), + } +} + +// SuccessResponse represents the success response structure +type SuccessResponse[T any] struct { + Success bool `json:"success"` + Data *T `json:"data,omitempty"` + Timestamp string `json:"timestamp"` +} + +type Params struct { + fx.In + Service service.Service +} + +func NewHandler(params Params) (*Handler, error) { + return &Handler{ + Service: params.Service, + }, nil +} + +type Handler struct { + Service service.Service +} + +func (h *Handler) JSONResponse(ctx context.Context, w http.ResponseWriter, status int, data any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + err := json.NewEncoder(w).Encode(data) + if err != nil { + logger.Logger(ctx).Error().Err(err).Msg("Failed to encode JSON response") + http.Error(w, "Failed to encode JSON response", http.StatusInternalServerError) + } +} + +func (h *Handler) JSONBind(r *http.Request, dst any) error { + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(dst) + if err != nil { + return err + } + return nil +} + +func (h *Handler) HandleError(ctx context.Context, w http.ResponseWriter, err error) { + httpErr, ok := errs.IsHTTPStatusError(err) + if ok { + h.ErrorResponse(ctx, w, httpErr.StatusCode, httpErr.Message, httpErr.OriginalErr) + return + } + h.ErrorResponse(ctx, w, http.StatusInternalServerError, "Internal Server Error", err) +} + +func (h *Handler) ErrorResponse(ctx context.Context, w http.ResponseWriter, status int, errMsg string, err error) { + if err != nil { + if status >= 500 { + logger.Logger(ctx).Error().Err(err).Msg(errMsg) + } else { + logger.Logger(ctx).Warn().Err(err).Msg(errMsg) + } + } + resp := ErrorResponse{ + Success: false, + Error: errMsg, + } + h.JSONResponse(ctx, w, status, resp) +} + +// Version godoc +// @Summary Get service version +// @Description Returns service version and exposed endpoints. +// @Tags System +// @Produce json +// @Success 200 {object} VersionResponse +// @Router /version [get] +func (h *Handler) Version(w http.ResponseWriter, r *http.Request) { + response := VersionResponse{ + Message: "BSS Metrics API Server", + Version: "1.0.0", + Endpoints: "/api/v1/auth/token (POST), /api/v1/metrics (POST), /api/v1/pods/pids (GET), /api/v1/scheduling/strategies (GET, POST), /health (GET), /static/ (Frontend)", + } + h.JSONResponse(r.Context(), w, http.StatusOK, response) +} + +// HealthCheck godoc +// @Summary Health check +// @Description Basic health check for readiness probes. +// @Tags System +// @Produce json +// @Success 200 {object} HealthResponse +// @Router /health [get] +func (h *Handler) HealthCheck(w http.ResponseWriter, r *http.Request) { + response := HealthResponse{ + Status: "healthy", + Timestamp: time.Now().UTC().Format(time.RFC3339), + Service: "BSS Metrics API Server", + } + h.JSONResponse(r.Context(), w, http.StatusOK, response) +} + +type HandleIntentsRequest struct { + Intents []Intent `json:"intents"` +} + +type Intent struct { + PodName string `json:"podName,omitempty"` + PodID string `json:"podID,omitempty"` + NodeID string `json:"nodeID,omitempty"` + K8sNamespace string `json:"k8sNamespace,omitempty"` + CommandRegex string `json:"commandRegex,omitempty"` + Priority int `json:"priority,omitempty"` + ExecutionTime int64 `json:"executionTime,omitempty"` + PodLabels map[string]string `json:"podLabels,omitempty"` +} + +func (h *Handler) HandleIntents(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var req HandleIntentsRequest + err := h.JSONBind(r, &req) + if err != nil { + h.ErrorResponse(ctx, w, http.StatusBadRequest, "Invalid request payload", err) + return + } + intents := make([]*domain.Intent, 0, len(req.Intents)) + for _, intent := range req.Intents { + intents = append(intents, &domain.Intent{ + PodName: intent.PodName, + PodID: intent.PodID, + NodeID: intent.NodeID, + K8sNamespace: intent.K8sNamespace, + CommandRegex: intent.CommandRegex, + Priority: intent.Priority, + ExecutionTime: intent.ExecutionTime, + PodLabels: intent.PodLabels, + }) + } + // TODO: forward intents to the ebpf user space agent + h.Service.ProcessIntents(r.Context(), intents) + h.JSONResponse(ctx, w, http.StatusOK, NewSuccessResponse[EmptyResponse](nil)) +} + +func (h *Handler) SetupRoutes(engine *echo.Echo) { + engine.GET("/health", h.echoHandler(h.HealthCheck)) + engine.GET("/version", h.echoHandler(h.Version)) + // docs.SwaggerInfo.BasePath = "/" + // engine.GET("/swagger/*", echoSwagger.WrapHandler) + + api := engine.Group("/api", echo.WrapMiddleware(middleware.LoggerMiddleware)) + // v1 routes + { + apiV1 := api.Group("/v1") + // auth routes + apiV1.POST("/intents", h.echoHandler(h.HandleIntents)) + } + +} + +func (h *Handler) echoHandler(handlerFunc func(w http.ResponseWriter, r *http.Request)) echo.HandlerFunc { + return echo.WrapHandler(http.HandlerFunc(handlerFunc)) +} diff --git a/decisionmaker/service/.keep b/decisionmaker/service/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/decisionmaker/service/service.go b/decisionmaker/service/service.go new file mode 100644 index 0000000..fa63d0e --- /dev/null +++ b/decisionmaker/service/service.go @@ -0,0 +1,178 @@ +package service + +import ( + "bufio" + "context" + "fmt" + "os" + "regexp" + "strconv" + "strings" + + "github.com/Gthulhu/api/decisionmaker/domain" + "github.com/Gthulhu/api/pkg/logger" +) + +func NewService() Service { + return Service{} +} + +type Service struct { +} + +const ( + procDir = "/proc" +) + +func (svc *Service) ProcessIntents(ctx context.Context, intents []*domain.Intent) error { + // Placeholder for processing intents + podInfos, err := svc.GetAllPodInfos(ctx) + if err != nil { + return err + } + for _, intent := range intents { + podInfo := podInfos[intent.PodID] + logger.Logger(ctx).Info().Msgf("Processing intent for PodName:%s PodID: %s on NodeID: %s, Process:%+v", intent.PodName, intent.PodID, intent.NodeID, podInfo) + // Add logic to handle the intent + + } + logger.Logger(ctx).Info().Msgf("Discovered pods: %+v", podInfos) + return nil +} + +// GetAllPodInfos retrieves all pod information by scanning the /proc filesystem +func (svc *Service) GetAllPodInfos(ctx context.Context) (map[string]*domain.PodInfo, error) { + return svc.FindPodInfoFrom(ctx, procDir) +} + +// FindPodInfoFrom scans the given rootDir (e.g., /proc) to find pod information +func (svc *Service) FindPodInfoFrom(ctx context.Context, rootDir string) (map[string]*domain.PodInfo, error) { + podMap := make(map[string]*domain.PodInfo) + + // Walk through /proc to find all processes + entries, err := os.ReadDir(rootDir) + if err != nil { + return nil, fmt.Errorf("failed to read /proc directory: %v", err) + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + // Check if directory name is a PID (numeric) + pid, err := strconv.Atoi(entry.Name()) + if err != nil { + // Not a numeric PID directory (e.g., "acpi", "bus", etc.) — skip + continue + } + + // Read cgroup information for this process + cgroupPath := fmt.Sprintf("%s/%d/cgroup", rootDir, pid) + file, err := os.Open(cgroupPath) + if err != nil { + logger.Logger(ctx).Warn().Err(err).Msgf("failed to open cgroup file for pid %d", pid) + continue + } + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + logger.Logger(ctx).Debug().Msgf("cgroup line for pid %d: %s", pid, line) + if strings.Contains(line, "kubepods") { + err = svc.parseCgroupToPodInfo(rootDir, line, pid, podMap) + if err != nil { + logger.Logger(ctx).Warn().Err(err).Msgf("failed to parse cgroup line for pid %d, line:%s", pid, line) + break + } + } + } + if err := scanner.Err(); err != nil { + } + _ = file.Close() + } + + return podMap, nil +} + +// parseCgroupToPodInfo parses a cgroup line (e.g // 0::/kubelet.slice/kubelet-kubepods.slice/kubelet-kubepods-pod20da609e_6973_4463_a1f9_2db9bcc5becc.slice/cri-containerd-10ec3c89629f71226b227e6510b2d465168b24005bbdcc5d7940517080830635.scope) to extract pod info and updates the podInfoMap +func (svc *Service) parseCgroupToPodInfo(rootDir string, line string, pid int, podInfoMap map[string]*domain.PodInfo) error { + parts := strings.Split(line, ":") + if len(parts) >= 3 { + cgroupHierarchy := parts[2] + + // Extract pod information + podUID, containerID, err := svc.getPodInfoFromCgroup(cgroupHierarchy) + if err != nil { + return err + } + + // Get process information + process, err := svc.getProcessInfo(rootDir, pid) + if err != nil { + return err + } + process.ContainerID = containerID + + // Create or update pod info + if podInfo, exists := podInfoMap[podUID]; exists { + podInfo.Processes = append(podInfo.Processes, process) + } else { + podInfoMap[podUID] = &domain.PodInfo{ + PodUID: podUID, + Processes: []domain.PodProcess{process}, + } + } + } + return nil +} + +var ( + podRegex = regexp.MustCompile(`pod([0-9a-fA-F_]+)(?:\.slice)?`) +) + +// getPodInfoFromCgroup extracts pod information from cgroup path +func (svc *Service) getPodInfoFromCgroup(cgroupPath string) (podUID string, containerID string, err error) { + // Parse cgroup path to extract pod information + // 0::/kubelet.slice/kubelet-kubepods.slice/kubelet-kubepods-pod20da609e_6973_4463_a1f9_2db9bcc5becc.slice/cri-containerd-10ec3c89629f71226b227e6510b2d465168b24005bbdcc5d7940517080830635.scope + parts := strings.Split(cgroupPath, "/") + for _, part := range parts { + if podRegex.MatchString(part) { + podUID = podRegex.FindStringSubmatch(part)[1] + podUID = strings.ReplaceAll(podUID, "_", "-") + } + if strings.HasPrefix(part, "cri-containerd-") && strings.HasSuffix(part, ".scope") { + containerID = strings.TrimPrefix(part, "cri-containerd-") + containerID = strings.TrimSuffix(containerID, ".scope") + } + } + + if podUID == "" { + return "", "", fmt.Errorf("pod UID not found in cgroup path") + } + + return podUID, containerID, nil +} + +// getProcessInfo reads process information from /proc// +func (svc *Service) getProcessInfo(rootDir string, pid int) (domain.PodProcess, error) { + process := domain.PodProcess{PID: pid} + + // Read command from /proc//comm + commPath := fmt.Sprintf("/%s/%d/comm", rootDir, pid) + if data, err := os.ReadFile(commPath); err == nil { + process.Command = strings.TrimSpace(string(data)) + } + + // Read PPID from /proc//stat + statPath := fmt.Sprintf("/%s/%d/stat", rootDir, pid) + if data, err := os.ReadFile(statPath); err == nil { + fields := strings.Fields(string(data)) + if len(fields) >= 4 { + if ppid, err := strconv.Atoi(fields[3]); err == nil { + process.PPID = ppid + } + } + } + + return process, nil +} diff --git a/decisionmaker/service/service_test.go b/decisionmaker/service/service_test.go new file mode 100644 index 0000000..fec96b6 --- /dev/null +++ b/decisionmaker/service/service_test.go @@ -0,0 +1,87 @@ +package service + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/Gthulhu/api/pkg/logger" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// setupFakeProcDir creates a temporary fake /proc directory structure for testing +func setupFakeProcDir(t *testing.T) string { + root := t.TempDir() + // fake pid 1234 + pidDir := filepath.Join(root, "1234") + if err := os.Mkdir(pidDir, 0755); err != nil { + t.Fatal(err) + } + + // cgroup + cgroupContent := "0::/kubelet.slice/kubelet-kubepods.slice/kubelet-kubepods-pod20da609e_6973_4463_a1f9_2db9bcc5becc.slice/cri-containerd-10ec3c89629f71226b227e6510b2d465168b24005bbdcc5d7940517080830635.scope\n" + if err := os.WriteFile(filepath.Join(pidDir, "cgroup"), []byte(cgroupContent), 0644); err != nil { + t.Fatal(err) + } + + // comm + if err := os.WriteFile(filepath.Join(pidDir, "comm"), []byte("nginx\n"), 0644); err != nil { + t.Fatal(err) + } + + // stat + statLine := "1234 (nginx) S 1 2 3 4 5 0 0 0 0 0 0 0 0 0 0 0 0 0 0" + if err := os.WriteFile(filepath.Join(pidDir, "stat"), []byte(statLine), 0644); err != nil { + t.Fatal(err) + } + + pid2Dir := filepath.Join(root, "5678") + if err := os.Mkdir(pid2Dir, 0755); err != nil { + t.Fatal(err) + } + + // cgroup for pid 5678 (not in kubepods) + cgroupContent2 := "0::/kubelet.slice/kubelet-kubepods.slice/kubelet-kubepods-besteffort.slice/kubelet-kubepods-besteffort-pode52d4a2a_6e5f_44d9_a8b8_37ff3daa7413.slice/cri-containerd-bc96d8a88e39e8be4ff9fc02f431c7db802002c1456a56166265f19d1a3cbbc3.scope\n" + if err := os.WriteFile(filepath.Join(pid2Dir, "cgroup"), []byte(cgroupContent2), 0644); err != nil { + t.Fatal(err) + } + + // comm for pid 5678 + if err := os.WriteFile(filepath.Join(pid2Dir, "comm"), []byte("busybox\n"), 0644); err != nil { + t.Fatal(err) + } + + // stat for pid 5678 + statLine2 := "5678 (busybox) S 1 2 3 4 5 0 0 0 0 0 0 0 0 0 0 0 0 0 0" + if err := os.WriteFile(filepath.Join(pid2Dir, "stat"), []byte(statLine2), 0644); err != nil { + t.Fatal(err) + } + + return root +} + +// TestFindPodInfoFrom tests the FindPodInfoFrom function +func TestFindPodInfoFrom(t *testing.T) { + logger.InitLogger() + fakeProc := setupFakeProcDir(t) + svc := &Service{} + + pods, err := svc.FindPodInfoFrom(context.Background(), fakeProc) + require.NoError(t, err, "FindPodInfoFrom should not return error") + require.Len(t, pods, 2, "should find one pod info") + p := pods["20da609e-6973-4463-a1f9-2db9bcc5becc"] + require.NotNil(t, p, "pod info should not be nil") + assert.EqualValues(t, p.PodUID, "20da609e-6973-4463-a1f9-2db9bcc5becc", "unexpected podUID") + assert.EqualValues(t, p.Processes[0].ContainerID, "10ec3c89629f71226b227e6510b2d465168b24005bbdcc5d7940517080830635", "unexpected containerID") + require.Len(t, p.Processes, 1, "should have one process") + assert.EqualValues(t, p.Processes[0].Command, "nginx", "unexpected command") + + p2 := pods["e52d4a2a-6e5f-44d9-a8b8-37ff3daa7413"] + require.NotNil(t, p2, "pod info should not be nil") + assert.EqualValues(t, p2.PodUID, "e52d4a2a-6e5f-44d9-a8b8-37ff3daa7413", "unexpected podUID") + assert.EqualValues(t, p2.Processes[0].ContainerID, "bc96d8a88e39e8be4ff9fc02f431c7db802002c1456a56166265f19d1a3cbbc3", "unexpected containerID") + require.Len(t, p2.Processes, 1, "should have one process") + assert.EqualValues(t, p2.Processes[0].Command, "busybox", "unexpected command") +} diff --git a/deployment/kind/decisonmaker/daemonset.yaml b/deployment/kind/decisonmaker/daemonset.yaml new file mode 100644 index 0000000..1446415 --- /dev/null +++ b/deployment/kind/decisonmaker/daemonset.yaml @@ -0,0 +1,104 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: decisionmaker + namespace: kube-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: decisionmaker +rules: + - apiGroups: [""] + resources: ["pods", "namespaces"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: decisionmaker +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: decisionmaker +subjects: + - kind: ServiceAccount + name: decisionmaker + namespace: kube-system +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: decisionmaker + namespace: kube-system + labels: + app: decisionmaker +spec: + selector: + matchLabels: + app: decisionmaker + updateStrategy: + type: RollingUpdate + template: + metadata: + labels: + app: decisionmaker + spec: + serviceAccountName: decisionmaker + hostPID: true + hostNetwork: false + containers: + - name: api + image: gthulhu-api:local + imagePullPolicy: IfNotPresent + command: + - /app/main + - decisionmaker + env: + - name: DM_SERVER_HOST + value: ":8080" + - name: DM_LOGGING_LEVEL + value: "info" + - name: TZ + value: UTC + securityContext: + privileged: true + allowPrivilegeEscalation: true + readOnlyRootFilesystem: false + ports: + - name: http + containerPort: 8080 + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 20 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 500m + memory: 256Mi + volumeMounts: + - name: proc-host + mountPath: /proc + readOnly: true + - name: var-run + mountPath: /var/run + volumes: + - name: proc-host + hostPath: + path: /proc + type: Directory + - name: var-run + hostPath: + path: /var/run + type: Directory \ No newline at end of file diff --git a/deployment/kind/decisonmaker/service.yaml b/deployment/kind/decisonmaker/service.yaml new file mode 100644 index 0000000..12bf510 --- /dev/null +++ b/deployment/kind/decisonmaker/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: decisionmaker + namespace: kube-system + labels: + app: decisionmaker +spec: + ports: + - port: 8080 + targetPort: 8080 + clusterIP: None + selector: + app: decisionmaker \ No newline at end of file diff --git a/deployment/kind/local_setup.sh b/deployment/kind/local_setup.sh new file mode 100644 index 0000000..05b99b6 --- /dev/null +++ b/deployment/kind/local_setup.sh @@ -0,0 +1,82 @@ +# Get the project root directory (3 levels up from deployment/kind/setup.sh) +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$PROJECT_ROOT" + + +CLUSTER="gthulhu-api-local" +NS="gthulhu-api-local" + +echo "Project root directory: $PROJECT_ROOT" + +echo "Setting up local kind cluster..." +# Verify if 'kind' is available +if command -v kind >/dev/null 2>&1; then + echo "kind found: $(kind version)" +else + echo "kind not found; will install via 'go install' next" + go install sigs.k8s.io/kind@v0.30.0 +fi + +if ! kind get clusters | grep -qx "$CLUSTER"; then + echo "Cluster '$CLUSTER' does not exist. Creating..." + kind create cluster --name "$CLUSTER" +else + echo "Cluster '$CLUSTER' already exists." +fi + +docker build -f $PROJECT_ROOT/Dockerfile.amd64 -t gthulhu-api:local . + +docker pull mongo:8.2.2 +kind load docker-image mongo:8.2.2 --name "$CLUSTER" +kind load docker-image gthulhu-api:local --name "$CLUSTER" + +kubectl get ns "$NS" >/dev/null 2>&1 || kubectl create ns "$NS" + +kubectl apply -n "$NS" -f "$PROJECT_ROOT/deployment/kind/mongo/secret.yaml" +kubectl apply -n "$NS" -f "$PROJECT_ROOT/deployment/kind/mongo/service.yaml" +kubectl apply -n "$NS" -f "$PROJECT_ROOT/deployment/kind/mongo/statefulset.yaml" + +# Wait for MongoDB StatefulSet to be ready +echo "Waiting for MongoDB StatefulSet to be ready..." +STS_NAME="" +for i in {1..30}; do + STS_NAME=$(kubectl -n "$NS" get statefulset -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) + [ -n "$STS_NAME" ] && break + sleep 2 +done +if [ -z "$STS_NAME" ]; then + echo "Failed to discover StatefulSet in namespace '$NS'" + exit 1 +fi +kubectl -n "$NS" rollout status "statefulset/$STS_NAME" --timeout=5m + +echo "deploy mongo" + +kubectl apply -f "$PROJECT_ROOT/deployment/kind/decisonmaker/service.yaml" +kubectl apply -f "$PROJECT_ROOT/deployment/kind/decisonmaker/daemonset.yaml" + +echo "deploy decisionmaker" + +kubectl apply -n "$NS" -f "$PROJECT_ROOT/deployment/kind/pod/busybox.yaml" + +echo "deploy busybox pods" + +kubectl apply -n "$NS" -f "$PROJECT_ROOT/deployment/kind/manager/service.yaml" +kubectl apply -n "$NS" -f "$PROJECT_ROOT/deployment/kind/manager/deployment.yaml" + +echo "Waiting for manager Deployment to be ready..." +DEPLOYMENT_NAME="" +for i in {1..30}; do + DEPLOYMENT_NAME=$(kubectl -n "$NS" get deployment -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) + [ -n "$DEPLOYMENT_NAME" ] && break + sleep 2 +done +if [ -z "$DEPLOYMENT_NAME" ]; then + echo "Failed to discover Deployment in namespace '$NS'" + exit 1 +fi +kubectl -n "$NS" rollout status "deployment/$DEPLOYMENT_NAME" --timeout=5m + +kubectl port-forward -n "$NS" svc/manager 8080:8080 & + +echo "Go to http://localhost:8080/swagger/index.html to access the Swagger UI for the manager API." \ No newline at end of file diff --git a/deployment/kind/local_teardown.sh b/deployment/kind/local_teardown.sh new file mode 100644 index 0000000..b934ec6 --- /dev/null +++ b/deployment/kind/local_teardown.sh @@ -0,0 +1,15 @@ +# Get the project root directory (3 levels up from deployment/kind/setup.sh) +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$PROJECT_ROOT" + + +CLUSTER="gthulhu-api-local" +NS="gthulhu-api-local" + +echo "Tearing down local kind cluster..." +if kind get clusters | grep -qx "$CLUSTER"; then + echo "Deleting cluster '$CLUSTER'..." + kind delete cluster --name "$CLUSTER" +else + echo "Cluster '$CLUSTER' does not exist. Nothing to delete." +fi \ No newline at end of file diff --git a/deployment/kind/manager/deployment.yaml b/deployment/kind/manager/deployment.yaml new file mode 100644 index 0000000..e6408a0 --- /dev/null +++ b/deployment/kind/manager/deployment.yaml @@ -0,0 +1,100 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: manager + namespace: gthulhu-api-local +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: manager +rules: + - apiGroups: [""] + resources: ["pods", "namespaces"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: manager +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager +subjects: + - kind: ServiceAccount + name: manager + namespace: gthulhu-api-local +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: gthulhu-api-local + name: manager +spec: + replicas: 1 + selector: + matchLabels: + app: manager + template: + metadata: + labels: + app: manager + spec: + serviceAccountName: manager + containers: + - name: manager + image: gthulhu-api:local + command: + - /app/main + - manager + imagePullPolicy: IfNotPresent + env: + - name: MANAGER_SERVER_HOST + value: ":8080" + - name: MANAGER_LOGGING_LEVEL + value: "info" + - name: MANAGER_MONGODB_HOST + value: "mongo-0.mongo.gthulhu-api-local.svc.cluster.local" + - name: MANAGER_MONGODB_PORT + value: "27017" + - name: MANAGER_MONGODB_DATABASE + value: "manager" + - name: MANAGER_MONGODB_CA_PEM + value: "" + - name: MANAGER_K8S_IN_CLUSTER + value: "true" + - name: MANAGER_MONGODB_USER + valueFrom: + secretKeyRef: + name: mongo-auth + key: MONGO_INITDB_ROOT_USERNAME + - name: MANAGER_MONGODB_PASSWORD + valueFrom: + secretKeyRef: + name: mongo-auth + key: MONGO_INITDB_ROOT_PASSWORD + - name: MANAGER_MONGODB_OPTIONS + value: "authSource=admin&tls=false&directConnection=true" + ports: + - name: http + containerPort: 8080 + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 20 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 500m + memory: 256Mi diff --git a/deployment/kind/manager/service.yaml b/deployment/kind/manager/service.yaml new file mode 100644 index 0000000..ee52563 --- /dev/null +++ b/deployment/kind/manager/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: manager + namespace: gthulhu-api-local + labels: + app: manager +spec: + selector: + app: manager + ports: + - name: http + port: 8080 + targetPort: 8080 diff --git a/deployment/kind/mongo/secret.yaml b/deployment/kind/mongo/secret.yaml new file mode 100644 index 0000000..1808f49 --- /dev/null +++ b/deployment/kind/mongo/secret.yaml @@ -0,0 +1,34 @@ +apiVersion: v1 +kind: Secret +metadata: + namespace: gthulhu-api-local + name: mongo-keyfile +type: Opaque +stringData: + keyfile: | + 4i7woinZw/LD3KQCXr6Kqrd1UOHLHVLkbPkzGbCSLLrSRu059nUXo4J9epVbwYB4 + xVAwBa4VawfpUXmcqoeXPcEXtfWamkhoNxPL6jBkrzc6nYIDSh/i8vlzG0OO834i + C60pwBNA0j1C0kjUG8xUgLV/rFoHYVDHNQVhgxuWSMwFcQH+QHERTz+R7DUY7GYj + /Y8ky2bmYuuyyXj7nyOpHerwDy95vmwInJFddJLw385hjc2bkHJTgBDDxXMagwgB + Kxf4LOOTcYoB6dabkjW0Y7DrXHo8FkIQQQKfxb/HjTjS3EXiJW1aPOdSAbiTmatP + qjuwPW9t8HX+8Q3WrseGBpSRbX8221xV6fcHRoSgDe2W6XL8kMegOgjDccJn5Z/0 + dniQ70vEeefaCDizRu6hm117a5feiqgOrqBteHR7llJ4Viz/QmMw4riby8c2N4ie + wWLanKk2qzCUXciefKy3IEgVqJE1yOgaKm805YA15ROvr68Nbke4A7cQVwK+bsRj + Ftj/Ts8uGr/pnKL4y1DVMRaNUldnJJgoyJEUspad9JW+iIDsn3G3giF1KdJjduP9 + xiGNyw4WQi75gETiFGJTEBDiZ500UibJ/P3AJTVIzC4lquybcYCi9vIinC5rj82q + SbhiS66P05fhHgvvbtEsVoBZ+oRiX9LGIFCx0/2iLfaum/6xR6mwCLtNVcuJCaGc + zwlEMDa7ihYOGnaErg9gpB6qfbzq3OlHleHEeEpNlr6d5vO6+QoP+czvqkWvqgSG + FjpfgVGOi2T8ckgQqTl+VMTcBVfFlxQu/lqFuxnHpmxf0IgVikpl2021FOtAil8G + 51/vYMb2qYpmkrxaYTN6fVmEH1mbKcxEMelCMg1f08bxY14HlHg/RU7Z3e4X78bf + JQOxvVmdzFSoCP7yekaoOYqV9ZaDQb5cWlR4sF41hB2mjunFQ4YP6OoiHg8/EJu5 + YbZ4byJMSn0Cphwx9CdQQHXOBSKRPC+8KLcSyPHxUM2B6hyD +--- +apiVersion: v1 +kind: Secret +metadata: + namespace: gthulhu-api-local + name: mongo-auth +type: Opaque +stringData: + MONGO_INITDB_ROOT_USERNAME: admin + MONGO_INITDB_ROOT_PASSWORD: test diff --git a/deployment/kind/mongo/service.yaml b/deployment/kind/mongo/service.yaml new file mode 100644 index 0000000..71b2b02 --- /dev/null +++ b/deployment/kind/mongo/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: mongo + namespace: gthulhu-api-local + labels: + app: mongo +spec: + ports: + - port: 27017 + targetPort: 27017 + clusterIP: None + selector: + app: mongo \ No newline at end of file diff --git a/deployment/kind/mongo/statefulset.yaml b/deployment/kind/mongo/statefulset.yaml new file mode 100644 index 0000000..5259ad9 --- /dev/null +++ b/deployment/kind/mongo/statefulset.yaml @@ -0,0 +1,187 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: mongo + namespace: gthulhu-api-local + labels: + app: mongo +spec: + serviceName: mongo + replicas: 1 + podManagementPolicy: OrderedReady + selector: + matchLabels: + app: mongo + template: + metadata: + labels: + app: mongo + spec: + terminationGracePeriodSeconds: 30 + + initContainers: + - name: fix-keyfile-permissions + image: busybox:1.36 + command: ["sh", "-c"] + args: + - | + cp /etc/mongo-keyfile-source/keyfile /etc/mongo-keyfile/keyfile + chmod 400 /etc/mongo-keyfile/keyfile + # 因為你的主容器是 runAsUser: 0 (root),這裡 owner 設為 root + chown 999:999 /etc/mongo-keyfile/keyfile + volumeMounts: + - name: keyfile-source + mountPath: /etc/mongo-keyfile-source + readOnly: true + - name: mongo-keyfile-processed + mountPath: /etc/mongo-keyfile + + containers: + - name: mongo + image: mongo:8.2.2 + imagePullPolicy: IfNotPresent + + # ---------------------- + # mongod + # ---------------------- + command: + - mongod + args: + - --bind_ip_all + - --replSet + - rs0 + - --dbpath + - /data/db + - --auth + - --keyFile + - /etc/mongo-keyfile/keyfile + + ports: + - name: mongo + containerPort: 27017 + + # ---------------------- + # 初始化:rs + admin user + # ---------------------- + lifecycle: + postStart: + exec: + command: + - /bin/sh + - -c + - | + set -e + + # 只在 mongo-0 初始化 + if [ "$(hostname)" != "mongo-0" ]; then + echo "Not mongo-0, skip init" + exit 0 + fi + + echo "[postStart] Waiting for mongod..." + until mongosh --host localhost:27017 --quiet --eval 'db.adminCommand({ ping: 1 })' >/dev/null 2>&1; do + sleep 2 + done + + echo "[postStart] Init replica set if needed..." + mongosh --host localhost:27017 --quiet --eval ' + try { + rs.status(); + print("Replica set already initialized"); + } catch (e) { + rs.initiate({ + _id: "rs0", + members: [ + { _id: 0, host: "mongo-0.mongo.gthulhu-api-local.svc.cluster.local:27017" } + ] + }); + print("Replica set initiated"); + } + db = db.getSiblingDB("admin"); + try { + db.getUser("'"$MONGO_ROOT_USERNAME"'"); + print("Admin user had been created"); + } catch (e) { + db.createUser({ + user: "'"$MONGO_ROOT_USERNAME"'", + pwd: "'"$MONGO_ROOT_PASSWORD"'", + roles: [{ role: "root", db: "admin" }] + }); + print("Admin user created"); + } + ' + + + + # ---------------------- + # Probes + # ---------------------- + readinessProbe: + tcpSocket: + port: 27017 + initialDelaySeconds: 10 + periodSeconds: 10 + + livenessProbe: + tcpSocket: + port: 27017 + initialDelaySeconds: 30 + periodSeconds: 10 + + # ---------------------- + # Volumes + # ---------------------- + volumeMounts: + - name: mongo-data + mountPath: /data/db + - name: mongo-keyfile-processed + mountPath: /etc/mongo-keyfile + readOnly: true + + # ---------------------- + # Env (帳密來自 Secret) + # ---------------------- + env: + - name: MONGO_ROOT_USERNAME + valueFrom: + secretKeyRef: + name: mongo-auth + key: MONGO_INITDB_ROOT_USERNAME + - name: MONGO_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: mongo-auth + key: MONGO_INITDB_ROOT_PASSWORD + + + # ---------------------- + # Security Context + # ---------------------- + securityContext: + runAsUser: 999 + runAsGroup: 999 + fsGroup: 999 + + # ---------------------- + # Volumes + # ---------------------- + volumes: + - name: keyfile-source + secret: + secretName: mongo-keyfile + defaultMode: 0400 + - name: mongo-keyfile-processed + emptyDir: {} + + # ---------------------- + # PVC + # ---------------------- + volumeClaimTemplates: + - metadata: + name: mongo-data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 5Gi + storageClassName: standard diff --git a/deployment/kind/pod/busybox.yaml b/deployment/kind/pod/busybox.yaml new file mode 100644 index 0000000..61f06a2 --- /dev/null +++ b/deployment/kind/pod/busybox.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: busybox + namespace: gthulhu-api-local + labels: + app: busybox + nf: upf +spec: + restartPolicy: Always + containers: + - name: busybox + image: busybox:1.36 + imagePullPolicy: IfNotPresent + command: ["sh", "-c", "sleep 1d"] \ No newline at end of file diff --git a/deployment/local/docker-compose.infra.yaml b/deployment/local/docker-compose.infra.yaml index 6b49f61..af85997 100644 --- a/deployment/local/docker-compose.infra.yaml +++ b/deployment/local/docker-compose.infra.yaml @@ -5,8 +5,52 @@ services: environment: MONGO_INITDB_ROOT_USERNAME: test MONGO_INITDB_ROOT_PASSWORD: test + volumes: + - ./mongo-keyfile:/etc/mongo-keyfile:ro ports: - 27017:27017 + command: ["mongod", "--auth", "--replSet", "rs0", "--bind_ip_all", "--keyFile", "/etc/mongo-keyfile"] + networks: + - mongo_cluster + healthcheck: + test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 + + mongo-setup: + image: mongo:8.2.2 + network_mode: host + depends_on: + - mongo + volumes: + - ./mongo-keyfile:/etc/mongo-keyfile:ro + entrypoint: [ + "/bin/sh", + "-c", + " + until mongosh --host localhost:27017 --quiet --eval 'db.adminCommand(\"ping\")' >/dev/null 2>&1; do + echo 'Waiting for mongod...'; + sleep 2; + done; + mongosh --host localhost:27017 --authenticationDatabase admin --quiet --eval ' + try { + rs.status(); + print(\"Replica set already initialized\"); + } catch (e) { + rs.initiate({ + _id: \"rs0\", + members: [ + { _id: 0, host: \"mongo:27017\" } + ] + }); + print(\"Replica set initiated\"); + } + '; + + echo 'MongoDB initialization done'; + " + ] networks: - mysql_cluster: + mongo_cluster: driver: bridge diff --git a/deployment/local/mongo-keyfile b/deployment/local/mongo-keyfile new file mode 100644 index 0000000..0d17a2f --- /dev/null +++ b/deployment/local/mongo-keyfile @@ -0,0 +1,16 @@ +4i7woinZw/LD3KQCXr6Kqrd1UOHLHVLkbPkzGbCSLLrSRu059nUXo4J9epVbwYB4 +xVAwBa4VawfpUXmcqoeXPcEXtfWamkhoNxPL6jBkrzc6nYIDSh/i8vlzG0OO834i +C60pwBNA0j1C0kjUG8xUgLV/rFoHYVDHNQVhgxuWSMwFcQH+QHERTz+R7DUY7GYj +/Y8ky2bmYuuyyXj7nyOpHerwDy95vmwInJFddJLw385hjc2bkHJTgBDDxXMagwgB +Kxf4LOOTcYoB6dabkjW0Y7DrXHo8FkIQQQKfxb/HjTjS3EXiJW1aPOdSAbiTmatP +qjuwPW9t8HX+8Q3WrseGBpSRbX8221xV6fcHRoSgDe2W6XL8kMegOgjDccJn5Z/0 +dniQ70vEeefaCDizRu6hm117a5feiqgOrqBteHR7llJ4Viz/QmMw4riby8c2N4ie +wWLanKk2qzCUXciefKy3IEgVqJE1yOgaKm805YA15ROvr68Nbke4A7cQVwK+bsRj +Ftj/Ts8uGr/pnKL4y1DVMRaNUldnJJgoyJEUspad9JW+iIDsn3G3giF1KdJjduP9 +xiGNyw4WQi75gETiFGJTEBDiZ500UibJ/P3AJTVIzC4lquybcYCi9vIinC5rj82q +SbhiS66P05fhHgvvbtEsVoBZ+oRiX9LGIFCx0/2iLfaum/6xR6mwCLtNVcuJCaGc +zwlEMDa7ihYOGnaErg9gpB6qfbzq3OlHleHEeEpNlr6d5vO6+QoP+czvqkWvqgSG +FjpfgVGOi2T8ckgQqTl+VMTcBVfFlxQu/lqFuxnHpmxf0IgVikpl2021FOtAil8G +51/vYMb2qYpmkrxaYTN6fVmEH1mbKcxEMelCMg1f08bxY14HlHg/RU7Z3e4X78bf +JQOxvVmdzFSoCP7yekaoOYqV9ZaDQb5cWlR4sF41hB2mjunFQ4YP6OoiHg8/EJu5 +YbZ4byJMSn0Cphwx9CdQQHXOBSKRPC+8KLcSyPHxUM2B6hyD diff --git a/docs/docs.go b/docs/manager/docs.go similarity index 60% rename from docs/docs.go rename to docs/manager/docs.go index 8540770..c780225 100644 --- a/docs/docs.go +++ b/docs/manager/docs.go @@ -1,15 +1,24 @@ -// Package docs Code generated by swaggo/swag. DO NOT EDIT -package docs +// Package manager Code generated by swaggo/swag. DO NOT EDIT +package manager import "github.com/swaggo/swag" const docTemplate = `{ "schemes": {{ marshal .Schemes }}, + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], "swagger": "2.0", "info": { "description": "{{escape .Description}}", "title": "{{.Title}}", - "contact": {}, + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support" + }, "version": "{{.Version}}" }, "host": "{{.Host}}", @@ -43,25 +52,77 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_LoginResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_LoginResponse" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "422": { "description": "Unprocessable Entity", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + } + } + } + }, + "/api/v1/intents/self": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "List schedule intents created by the authenticated user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Strategies" + ], + "summary": "List self schedule intents", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListScheduleIntentsResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -86,19 +147,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_ListPermissionsResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListPermissionsResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -123,19 +184,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_ListRolesResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListRolesResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -172,31 +233,31 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -233,31 +294,31 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -294,31 +355,146 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + } + } + } + }, + "/api/v1/strategies": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a new schedule strategy.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Strategies" + ], + "summary": "Create schedule strategy", + "parameters": [ + { + "description": "Schedule strategy payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rest.CreateScheduleStrategyRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + } + } + } + }, + "/api/v1/strategies/self": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "List schedule strategies created by the authenticated user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Strategies" + ], + "summary": "List self schedule strategies", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListSchedulerStrategiesResponse" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -343,19 +519,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_ListUsersResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListUsersResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -392,31 +568,31 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -455,31 +631,31 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -518,31 +694,31 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -567,19 +743,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_GetSelfUserResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_GetSelfUserResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -618,25 +794,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -656,7 +832,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.HealthResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.HealthResponse" } } } @@ -676,7 +852,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.VersionResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.VersionResponse" } } } @@ -684,6 +860,19 @@ const docTemplate = `{ } }, "definitions": { + "domain.IntentState": { + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "IntentStateUnknown", + "IntentStateInitialized", + "IntentStateSent" + ] + }, "domain.PermissionKey": { "type": "string", "enum": [ @@ -695,7 +884,10 @@ const docTemplate = `{ "role.read", "role.update", "role.delete", - "permission.read" + "permission.read", + "schedule_strategy.create", + "schedule_strategy.read", + "schedule_intent.read" ], "x-enum-varnames": [ "CreateUser", @@ -706,12 +898,14 @@ const docTemplate = `{ "RoleRead", "RoleUpdate", "RoleDelete", - "PermissionRead" + "PermissionRead", + "ScheduleStrategyCreate", + "ScheduleStrategyRead", + "ScheduleIntentRead" ] }, "domain.UserStatus": { "type": "integer", - "format": "int32", "enum": [ 1, 2, @@ -723,6 +917,188 @@ const docTemplate = `{ "UserStatusWaitChangePassword" ] }, + "github_com_Gthulhu_api_decisionmaker_rest.HealthResponse": { + "type": "object", + "properties": { + "service": { + "type": "string" + }, + "status": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, + "github_com_Gthulhu_api_decisionmaker_rest.VersionResponse": { + "type": "object", + "properties": { + "endpoints": { + "type": "string" + }, + "message": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "github_com_Gthulhu_api_manager_rest.EmptyResponse": { + "type": "object" + }, + "github_com_Gthulhu_api_manager_rest.ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + }, + "github_com_Gthulhu_api_manager_rest.HealthResponse": { + "type": "object", + "properties": { + "service": { + "type": "string" + }, + "status": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, + "github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.EmptyResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_GetSelfUserResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.GetSelfUserResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListPermissionsResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.ListPermissionsResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListRolesResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.ListRolesResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListScheduleIntentsResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.ListScheduleIntentsResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListSchedulerStrategiesResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.ListSchedulerStrategiesResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListUsersResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.ListUsersResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_LoginResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.LoginResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "github_com_Gthulhu_api_manager_rest.VersionResponse": { + "type": "object", + "properties": { + "endpoints": { + "type": "string" + }, + "message": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, "rest.ChangePasswordRequest": { "type": "object", "properties": { @@ -751,36 +1127,51 @@ const docTemplate = `{ } } }, - "rest.CreateUserRequest": { + "rest.CreateScheduleStrategyRequest": { "type": "object", "properties": { - "password": { + "commandRegex": { "type": "string" }, - "username": { + "executionTime": { + "type": "integer" + }, + "k8sNamespace": { + "type": "array", + "items": { + "type": "string" + } + }, + "labelSelectors": { + "type": "array", + "items": { + "$ref": "#/definitions/rest.LabelSelector" + } + }, + "priority": { + "type": "integer" + }, + "strategyNamespace": { "type": "string" } } }, - "rest.DeleteRoleRequest": { + "rest.CreateUserRequest": { "type": "object", "properties": { - "id": { + "password": { + "type": "string" + }, + "username": { "type": "string" } } }, - "rest.EmptyResponse": { - "type": "object" - }, - "rest.ErrorResponse": { + "rest.DeleteRoleRequest": { "type": "object", "properties": { - "error": { + "id": { "type": "string" - }, - "success": { - "type": "boolean" } } }, @@ -804,16 +1195,13 @@ const docTemplate = `{ } } }, - "rest.HealthResponse": { + "rest.LabelSelector": { "type": "object", "properties": { - "service": { - "type": "string" - }, - "status": { + "key": { "type": "string" }, - "timestamp": { + "value": { "type": "string" } } @@ -865,6 +1253,28 @@ const docTemplate = `{ } } }, + "rest.ListScheduleIntentsResponse": { + "type": "object", + "properties": { + "intents": { + "type": "array", + "items": { + "$ref": "#/definitions/rest.ScheduleIntent" + } + } + } + }, + "rest.ListSchedulerStrategiesResponse": { + "type": "object", + "properties": { + "strategies": { + "type": "array", + "items": { + "$ref": "#/definitions/rest.ScheduleStrategy" + } + } + } + }, "rest.ListUsersResponse": { "type": "object", "properties": { @@ -940,86 +1350,72 @@ const docTemplate = `{ } } }, - "rest.SuccessResponse-rest_EmptyResponse": { + "rest.ScheduleIntent": { "type": "object", "properties": { - "data": { - "$ref": "#/definitions/rest.EmptyResponse" + "commandRegex": { + "type": "string" }, - "success": { - "type": "boolean" + "executionTime": { + "type": "integer" }, - "timestamp": { + "id": { "type": "string" - } - } - }, - "rest.SuccessResponse-rest_GetSelfUserResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/rest.GetSelfUserResponse" - }, - "success": { - "type": "boolean" }, - "timestamp": { + "k8sNamespace": { "type": "string" - } - } - }, - "rest.SuccessResponse-rest_ListPermissionsResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/rest.ListPermissionsResponse" }, - "success": { - "type": "boolean" + "nodeID": { + "type": "string" }, - "timestamp": { + "podID": { "type": "string" - } - } - }, - "rest.SuccessResponse-rest_ListRolesResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/rest.ListRolesResponse" }, - "success": { - "type": "boolean" + "podLabels": { + "type": "object", + "additionalProperties": { + "type": "string" + } }, - "timestamp": { + "priority": { + "type": "integer" + }, + "state": { + "$ref": "#/definitions/domain.IntentState" + }, + "strategyID": { "type": "string" } } }, - "rest.SuccessResponse-rest_ListUsersResponse": { + "rest.ScheduleStrategy": { "type": "object", "properties": { - "data": { - "$ref": "#/definitions/rest.ListUsersResponse" + "commandRegex": { + "type": "string" }, - "success": { - "type": "boolean" + "executionTime": { + "type": "integer" }, - "timestamp": { + "id": { "type": "string" - } - } - }, - "rest.SuccessResponse-rest_LoginResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/rest.LoginResponse" }, - "success": { - "type": "boolean" + "k8sNamespace": { + "type": "array", + "items": { + "type": "string" + } }, - "timestamp": { + "labelSelectors": { + "type": "array", + "items": { + "$ref": "#/definitions/rest.LabelSelector" + } + }, + "priority": { + "type": "integer" + }, + "strategyNamespace": { "type": "string" } } @@ -1060,32 +1456,29 @@ const docTemplate = `{ "type": "string" } } - }, - "rest.VersionResponse": { - "type": "object", - "properties": { - "endpoints": { - "type": "string" - }, - "message": { - "type": "string" - }, - "version": { - "type": "string" - } - } } + }, + "securityDefinitions": { + "BearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + }, + "externalDocs": { + "description": "OpenAPI", + "url": "https://swagger.io/resources/open-api/" } }` // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ - Version: "", - Host: "", - BasePath: "", - Schemes: []string{}, - Title: "", - Description: "", + Version: "1.0", + Host: "localhost:8080", + BasePath: "/", + Schemes: []string{"http"}, + Title: "manager service", + Description: "manager service API documentation", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", diff --git a/docs/swagger.json b/docs/manager/swagger.json similarity index 60% rename from docs/swagger.json rename to docs/manager/swagger.json index dde18b0..04f8db6 100644 --- a/docs/swagger.json +++ b/docs/manager/swagger.json @@ -1,8 +1,25 @@ { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "schemes": [ + "http" + ], "swagger": "2.0", "info": { - "contact": {} + "description": "manager service API documentation", + "title": "manager service", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support" + }, + "version": "1.0" }, + "host": "localhost:8080", + "basePath": "/", "paths": { "/api/v1/auth/login": { "post": { @@ -32,25 +49,77 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_LoginResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_LoginResponse" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "422": { "description": "Unprocessable Entity", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + } + } + } + }, + "/api/v1/intents/self": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "List schedule intents created by the authenticated user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Strategies" + ], + "summary": "List self schedule intents", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListScheduleIntentsResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -75,19 +144,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_ListPermissionsResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListPermissionsResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -112,19 +181,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_ListRolesResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListRolesResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -161,31 +230,31 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -222,31 +291,31 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -283,31 +352,146 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + } + } + } + }, + "/api/v1/strategies": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a new schedule strategy.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Strategies" + ], + "summary": "Create schedule strategy", + "parameters": [ + { + "description": "Schedule strategy payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rest.CreateScheduleStrategyRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" + } + } + } + } + }, + "/api/v1/strategies/self": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "List schedule strategies created by the authenticated user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Strategies" + ], + "summary": "List self schedule strategies", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListSchedulerStrategiesResponse" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -332,19 +516,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_ListUsersResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListUsersResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -381,31 +565,31 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -444,31 +628,31 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -507,31 +691,31 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -556,19 +740,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_GetSelfUserResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_GetSelfUserResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -607,25 +791,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.SuccessResponse-rest_EmptyResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/rest.ErrorResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse" } } } @@ -645,7 +829,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.HealthResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.HealthResponse" } } } @@ -665,7 +849,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/rest.VersionResponse" + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.VersionResponse" } } } @@ -673,6 +857,19 @@ } }, "definitions": { + "domain.IntentState": { + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "IntentStateUnknown", + "IntentStateInitialized", + "IntentStateSent" + ] + }, "domain.PermissionKey": { "type": "string", "enum": [ @@ -684,7 +881,10 @@ "role.read", "role.update", "role.delete", - "permission.read" + "permission.read", + "schedule_strategy.create", + "schedule_strategy.read", + "schedule_intent.read" ], "x-enum-varnames": [ "CreateUser", @@ -695,12 +895,14 @@ "RoleRead", "RoleUpdate", "RoleDelete", - "PermissionRead" + "PermissionRead", + "ScheduleStrategyCreate", + "ScheduleStrategyRead", + "ScheduleIntentRead" ] }, "domain.UserStatus": { "type": "integer", - "format": "int32", "enum": [ 1, 2, @@ -712,6 +914,188 @@ "UserStatusWaitChangePassword" ] }, + "github_com_Gthulhu_api_decisionmaker_rest.HealthResponse": { + "type": "object", + "properties": { + "service": { + "type": "string" + }, + "status": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, + "github_com_Gthulhu_api_decisionmaker_rest.VersionResponse": { + "type": "object", + "properties": { + "endpoints": { + "type": "string" + }, + "message": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "github_com_Gthulhu_api_manager_rest.EmptyResponse": { + "type": "object" + }, + "github_com_Gthulhu_api_manager_rest.ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + }, + "github_com_Gthulhu_api_manager_rest.HealthResponse": { + "type": "object", + "properties": { + "service": { + "type": "string" + }, + "status": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, + "github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_Gthulhu_api_manager_rest.EmptyResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_GetSelfUserResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.GetSelfUserResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListPermissionsResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.ListPermissionsResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListRolesResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.ListRolesResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListScheduleIntentsResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.ListScheduleIntentsResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListSchedulerStrategiesResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.ListSchedulerStrategiesResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListUsersResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.ListUsersResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_LoginResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/rest.LoginResponse" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + } + } + }, + "github_com_Gthulhu_api_manager_rest.VersionResponse": { + "type": "object", + "properties": { + "endpoints": { + "type": "string" + }, + "message": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, "rest.ChangePasswordRequest": { "type": "object", "properties": { @@ -740,36 +1124,51 @@ } } }, - "rest.CreateUserRequest": { + "rest.CreateScheduleStrategyRequest": { "type": "object", "properties": { - "password": { + "commandRegex": { "type": "string" }, - "username": { + "executionTime": { + "type": "integer" + }, + "k8sNamespace": { + "type": "array", + "items": { + "type": "string" + } + }, + "labelSelectors": { + "type": "array", + "items": { + "$ref": "#/definitions/rest.LabelSelector" + } + }, + "priority": { + "type": "integer" + }, + "strategyNamespace": { "type": "string" } } }, - "rest.DeleteRoleRequest": { + "rest.CreateUserRequest": { "type": "object", "properties": { - "id": { + "password": { + "type": "string" + }, + "username": { "type": "string" } } }, - "rest.EmptyResponse": { - "type": "object" - }, - "rest.ErrorResponse": { + "rest.DeleteRoleRequest": { "type": "object", "properties": { - "error": { + "id": { "type": "string" - }, - "success": { - "type": "boolean" } } }, @@ -793,16 +1192,13 @@ } } }, - "rest.HealthResponse": { + "rest.LabelSelector": { "type": "object", "properties": { - "service": { - "type": "string" - }, - "status": { + "key": { "type": "string" }, - "timestamp": { + "value": { "type": "string" } } @@ -854,6 +1250,28 @@ } } }, + "rest.ListScheduleIntentsResponse": { + "type": "object", + "properties": { + "intents": { + "type": "array", + "items": { + "$ref": "#/definitions/rest.ScheduleIntent" + } + } + } + }, + "rest.ListSchedulerStrategiesResponse": { + "type": "object", + "properties": { + "strategies": { + "type": "array", + "items": { + "$ref": "#/definitions/rest.ScheduleStrategy" + } + } + } + }, "rest.ListUsersResponse": { "type": "object", "properties": { @@ -929,86 +1347,72 @@ } } }, - "rest.SuccessResponse-rest_EmptyResponse": { + "rest.ScheduleIntent": { "type": "object", "properties": { - "data": { - "$ref": "#/definitions/rest.EmptyResponse" + "commandRegex": { + "type": "string" }, - "success": { - "type": "boolean" + "executionTime": { + "type": "integer" }, - "timestamp": { + "id": { "type": "string" - } - } - }, - "rest.SuccessResponse-rest_GetSelfUserResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/rest.GetSelfUserResponse" - }, - "success": { - "type": "boolean" }, - "timestamp": { + "k8sNamespace": { "type": "string" - } - } - }, - "rest.SuccessResponse-rest_ListPermissionsResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/rest.ListPermissionsResponse" }, - "success": { - "type": "boolean" + "nodeID": { + "type": "string" }, - "timestamp": { + "podID": { "type": "string" - } - } - }, - "rest.SuccessResponse-rest_ListRolesResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/rest.ListRolesResponse" }, - "success": { - "type": "boolean" + "podLabels": { + "type": "object", + "additionalProperties": { + "type": "string" + } }, - "timestamp": { + "priority": { + "type": "integer" + }, + "state": { + "$ref": "#/definitions/domain.IntentState" + }, + "strategyID": { "type": "string" } } }, - "rest.SuccessResponse-rest_ListUsersResponse": { + "rest.ScheduleStrategy": { "type": "object", "properties": { - "data": { - "$ref": "#/definitions/rest.ListUsersResponse" + "commandRegex": { + "type": "string" }, - "success": { - "type": "boolean" + "executionTime": { + "type": "integer" }, - "timestamp": { + "id": { "type": "string" - } - } - }, - "rest.SuccessResponse-rest_LoginResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/rest.LoginResponse" }, - "success": { - "type": "boolean" + "k8sNamespace": { + "type": "array", + "items": { + "type": "string" + } }, - "timestamp": { + "labelSelectors": { + "type": "array", + "items": { + "$ref": "#/definitions/rest.LabelSelector" + } + }, + "priority": { + "type": "integer" + }, + "strategyNamespace": { "type": "string" } } @@ -1049,20 +1453,17 @@ "type": "string" } } - }, - "rest.VersionResponse": { - "type": "object", - "properties": { - "endpoints": { - "type": "string" - }, - "message": { - "type": "string" - }, - "version": { - "type": "string" - } - } } + }, + "securityDefinitions": { + "BearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + }, + "externalDocs": { + "description": "OpenAPI", + "url": "https://swagger.io/resources/open-api/" } } \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/manager/swagger.yaml similarity index 51% rename from docs/swagger.yaml rename to docs/manager/swagger.yaml index c9481c3..9e0179a 100644 --- a/docs/swagger.yaml +++ b/docs/manager/swagger.yaml @@ -1,4 +1,17 @@ +basePath: / +consumes: +- application/json definitions: + domain.IntentState: + enum: + - 0 + - 1 + - 2 + type: integer + x-enum-varnames: + - IntentStateUnknown + - IntentStateInitialized + - IntentStateSent domain.PermissionKey: enum: - user.create @@ -10,6 +23,9 @@ definitions: - role.update - role.delete - permission.read + - schedule_strategy.create + - schedule_strategy.read + - schedule_intent.read type: string x-enum-varnames: - CreateUser @@ -21,17 +37,136 @@ definitions: - RoleUpdate - RoleDelete - PermissionRead + - ScheduleStrategyCreate + - ScheduleStrategyRead + - ScheduleIntentRead domain.UserStatus: enum: - 1 - 2 - 3 - format: int32 type: integer x-enum-varnames: - UserStatusActive - UserStatusInactive - UserStatusWaitChangePassword + github_com_Gthulhu_api_decisionmaker_rest.HealthResponse: + properties: + service: + type: string + status: + type: string + timestamp: + type: string + type: object + github_com_Gthulhu_api_decisionmaker_rest.VersionResponse: + properties: + endpoints: + type: string + message: + type: string + version: + type: string + type: object + github_com_Gthulhu_api_manager_rest.EmptyResponse: + type: object + github_com_Gthulhu_api_manager_rest.ErrorResponse: + properties: + error: + type: string + success: + type: boolean + type: object + github_com_Gthulhu_api_manager_rest.HealthResponse: + properties: + service: + type: string + status: + type: string + timestamp: + type: string + type: object + github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse: + properties: + data: + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.EmptyResponse' + success: + type: boolean + timestamp: + type: string + type: object + github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_GetSelfUserResponse: + properties: + data: + $ref: '#/definitions/rest.GetSelfUserResponse' + success: + type: boolean + timestamp: + type: string + type: object + github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListPermissionsResponse: + properties: + data: + $ref: '#/definitions/rest.ListPermissionsResponse' + success: + type: boolean + timestamp: + type: string + type: object + github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListRolesResponse: + properties: + data: + $ref: '#/definitions/rest.ListRolesResponse' + success: + type: boolean + timestamp: + type: string + type: object + github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListScheduleIntentsResponse: + properties: + data: + $ref: '#/definitions/rest.ListScheduleIntentsResponse' + success: + type: boolean + timestamp: + type: string + type: object + github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListSchedulerStrategiesResponse: + properties: + data: + $ref: '#/definitions/rest.ListSchedulerStrategiesResponse' + success: + type: boolean + timestamp: + type: string + type: object + github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListUsersResponse: + properties: + data: + $ref: '#/definitions/rest.ListUsersResponse' + success: + type: boolean + timestamp: + type: string + type: object + github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_LoginResponse: + properties: + data: + $ref: '#/definitions/rest.LoginResponse' + success: + type: boolean + timestamp: + type: string + type: object + github_com_Gthulhu_api_manager_rest.VersionResponse: + properties: + endpoints: + type: string + message: + type: string + version: + type: string + type: object rest.ChangePasswordRequest: properties: newPassword: @@ -50,6 +185,25 @@ definitions: $ref: '#/definitions/rest.RolePolicy' type: array type: object + rest.CreateScheduleStrategyRequest: + properties: + commandRegex: + type: string + executionTime: + type: integer + k8sNamespace: + items: + type: string + type: array + labelSelectors: + items: + $ref: '#/definitions/rest.LabelSelector' + type: array + priority: + type: integer + strategyNamespace: + type: string + type: object rest.CreateUserRequest: properties: password: @@ -62,15 +216,6 @@ definitions: id: type: string type: object - rest.EmptyResponse: - type: object - rest.ErrorResponse: - properties: - error: - type: string - success: - type: boolean - type: object rest.GetSelfUserResponse: properties: id: @@ -84,13 +229,11 @@ definitions: username: type: string type: object - rest.HealthResponse: + rest.LabelSelector: properties: - service: + key: type: string - status: - type: string - timestamp: + value: type: string type: object rest.ListPermissionsResponse: @@ -123,6 +266,20 @@ definitions: type: object type: array type: object + rest.ListScheduleIntentsResponse: + properties: + intents: + items: + $ref: '#/definitions/rest.ScheduleIntent' + type: array + type: object + rest.ListSchedulerStrategiesResponse: + properties: + strategies: + items: + $ref: '#/definitions/rest.ScheduleStrategy' + type: array + type: object rest.ListUsersResponse: properties: users: @@ -171,58 +328,50 @@ definitions: self: type: boolean type: object - rest.SuccessResponse-rest_EmptyResponse: + rest.ScheduleIntent: properties: - data: - $ref: '#/definitions/rest.EmptyResponse' - success: - type: boolean - timestamp: + commandRegex: type: string - type: object - rest.SuccessResponse-rest_GetSelfUserResponse: - properties: - data: - $ref: '#/definitions/rest.GetSelfUserResponse' - success: - type: boolean - timestamp: + executionTime: + type: integer + id: type: string - type: object - rest.SuccessResponse-rest_ListPermissionsResponse: - properties: - data: - $ref: '#/definitions/rest.ListPermissionsResponse' - success: - type: boolean - timestamp: + k8sNamespace: type: string - type: object - rest.SuccessResponse-rest_ListRolesResponse: - properties: - data: - $ref: '#/definitions/rest.ListRolesResponse' - success: - type: boolean - timestamp: + nodeID: type: string - type: object - rest.SuccessResponse-rest_ListUsersResponse: - properties: - data: - $ref: '#/definitions/rest.ListUsersResponse' - success: - type: boolean - timestamp: + podID: + type: string + podLabels: + additionalProperties: + type: string + type: object + priority: + type: integer + state: + $ref: '#/definitions/domain.IntentState' + strategyID: type: string type: object - rest.SuccessResponse-rest_LoginResponse: + rest.ScheduleStrategy: properties: - data: - $ref: '#/definitions/rest.LoginResponse' - success: - type: boolean - timestamp: + commandRegex: + type: string + executionTime: + type: integer + id: + type: string + k8sNamespace: + items: + type: string + type: array + labelSelectors: + items: + $ref: '#/definitions/rest.LabelSelector' + type: array + priority: + type: integer + strategyNamespace: type: string type: object rest.UpdateRoleRequest: @@ -249,17 +398,17 @@ definitions: userID: type: string type: object - rest.VersionResponse: - properties: - endpoints: - type: string - message: - type: string - version: - type: string - type: object +externalDocs: + description: OpenAPI + url: https://swagger.io/resources/open-api/ +host: localhost:8080 info: - contact: {} + contact: + name: API Support + description: manager service API documentation + termsOfService: http://swagger.io/terms/ + title: manager service + version: "1.0" paths: /api/v1/auth/login: post: @@ -279,22 +428,55 @@ paths: "200": description: OK schema: - $ref: '#/definitions/rest.SuccessResponse-rest_LoginResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_LoginResponse' "400": description: Bad Request schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "422": description: Unprocessable Entity schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "500": description: Internal Server Error schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' summary: User login tags: - Auth + /api/v1/intents/self: + get: + consumes: + - application/json + description: List schedule intents created by the authenticated user. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListScheduleIntentsResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' + security: + - BearerAuth: [] + summary: List self schedule intents + tags: + - Strategies /api/v1/permissions: get: description: Retrieve all permission keys. @@ -304,15 +486,15 @@ paths: "200": description: OK schema: - $ref: '#/definitions/rest.SuccessResponse-rest_ListPermissionsResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListPermissionsResponse' "401": description: Unauthorized schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "500": description: Internal Server Error schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' security: - BearerAuth: [] summary: List permissions @@ -336,23 +518,23 @@ paths: "200": description: OK schema: - $ref: '#/definitions/rest.SuccessResponse-rest_EmptyResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse' "400": description: Bad Request schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "401": description: Unauthorized schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "403": description: Forbidden schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "500": description: Internal Server Error schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' security: - BearerAuth: [] summary: Delete role @@ -366,15 +548,15 @@ paths: "200": description: OK schema: - $ref: '#/definitions/rest.SuccessResponse-rest_ListRolesResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListRolesResponse' "401": description: Unauthorized schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "500": description: Internal Server Error schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' security: - BearerAuth: [] summary: List roles @@ -397,23 +579,23 @@ paths: "200": description: OK schema: - $ref: '#/definitions/rest.SuccessResponse-rest_EmptyResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse' "400": description: Bad Request schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "401": description: Unauthorized schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "403": description: Forbidden schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "500": description: Internal Server Error schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' security: - BearerAuth: [] summary: Create role @@ -436,28 +618,101 @@ paths: "200": description: OK schema: - $ref: '#/definitions/rest.SuccessResponse-rest_EmptyResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse' "400": description: Bad Request schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "401": description: Unauthorized schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "403": description: Forbidden schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "500": description: Internal Server Error schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' security: - BearerAuth: [] summary: Update role tags: - Roles + /api/v1/strategies: + post: + consumes: + - application/json + description: Create a new schedule strategy. + parameters: + - description: Schedule strategy payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/rest.CreateScheduleStrategyRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' + security: + - BearerAuth: [] + summary: Create schedule strategy + tags: + - Strategies + /api/v1/strategies/self: + get: + consumes: + - application/json + description: List schedule strategies created by the authenticated user. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListSchedulerStrategiesResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' + security: + - BearerAuth: [] + summary: List self schedule strategies + tags: + - Strategies /api/v1/users: get: description: Retrieve user list. @@ -467,15 +722,15 @@ paths: "200": description: OK schema: - $ref: '#/definitions/rest.SuccessResponse-rest_ListUsersResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_ListUsersResponse' "401": description: Unauthorized schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "500": description: Internal Server Error schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' security: - BearerAuth: [] summary: List users @@ -498,23 +753,23 @@ paths: "200": description: OK schema: - $ref: '#/definitions/rest.SuccessResponse-rest_EmptyResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse' "400": description: Bad Request schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "401": description: Unauthorized schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "403": description: Forbidden schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "500": description: Internal Server Error schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' security: - BearerAuth: [] summary: Create user @@ -538,23 +793,23 @@ paths: "200": description: OK schema: - $ref: '#/definitions/rest.SuccessResponse-rest_EmptyResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse' "400": description: Bad Request schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "401": description: Unauthorized schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "403": description: Forbidden schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "500": description: Internal Server Error schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' security: - BearerAuth: [] summary: Reset user password @@ -578,23 +833,23 @@ paths: "200": description: OK schema: - $ref: '#/definitions/rest.SuccessResponse-rest_EmptyResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse' "400": description: Bad Request schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "401": description: Unauthorized schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "403": description: Forbidden schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "500": description: Internal Server Error schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' security: - BearerAuth: [] summary: Update user roles and status @@ -609,15 +864,15 @@ paths: "200": description: OK schema: - $ref: '#/definitions/rest.SuccessResponse-rest_GetSelfUserResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-rest_GetSelfUserResponse' "401": description: Unauthorized schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "500": description: Internal Server Error schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' security: - BearerAuth: [] summary: Get current user @@ -641,19 +896,19 @@ paths: "200": description: OK schema: - $ref: '#/definitions/rest.SuccessResponse-rest_EmptyResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.SuccessResponse-github_com_Gthulhu_api_manager_rest_EmptyResponse' "400": description: Bad Request schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "401": description: Unauthorized schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' "500": description: Internal Server Error schema: - $ref: '#/definitions/rest.ErrorResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.ErrorResponse' security: - BearerAuth: [] summary: Change own password @@ -668,7 +923,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/rest.HealthResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.HealthResponse' summary: Health check tags: - System @@ -681,8 +936,17 @@ paths: "200": description: OK schema: - $ref: '#/definitions/rest.VersionResponse' + $ref: '#/definitions/github_com_Gthulhu_api_manager_rest.VersionResponse' summary: Get service version tags: - System +produces: +- application/json +schemes: +- http +securityDefinitions: + BearerAuth: + in: header + name: Authorization + type: apiKey swagger: "2.0" diff --git a/main.go b/main.go index 9160d07..f7886dd 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,11 @@ package main import ( - "context" "log" "os" - managerapp "github.com/Gthulhu/api/manager/app" - "github.com/Gthulhu/api/pkg/logger" + dmcmd "github.com/Gthulhu/api/decisionmaker/cmd" + managercmd "github.com/Gthulhu/api/manager/cmd" "github.com/spf13/cobra" ) @@ -14,53 +13,10 @@ var ( rootCmd = &cobra.Command{Use: "manager or decisionmaker"} ) -func init() { - ManagerCmd.Flags().StringP("config-name", "c", "", "Configuration file name without extension") - ManagerCmd.Flags().StringP("config-dir", "d", "", "Configuration file directory path") -} - func main() { - rootCmd.AddCommand(ManagerCmd) + rootCmd.AddCommand(managercmd.ManagerCmd, dmcmd.DMCmd) if err := rootCmd.Execute(); err != nil { log.Fatalf("Command execution failed: %v", err) os.Exit(1) } } - -// GrpcServerCmd is the command to start the gRPC server -var ManagerCmd = &cobra.Command{ - Run: RunManagerApp, - Use: "manager", -} - -func RunManagerApp(cmd *cobra.Command, args []string) { - configName, configDirPath := getConfigInfo(cmd) - logger.InitLogger() - app, err := managerapp.NewRestApp(configName, configDirPath) - if err != nil { - logger.Logger(context.Background()).Fatal().Err(err).Msg("failed to create rest app") - } - app.Run() -} - -func getConfigInfo(cmd *cobra.Command) (string, string) { - configName := "manager_config" - configDirPath := "" - if cmd != nil { - configNameFlag, err := cmd.Flags().GetString("config-name") - if err == nil && configNameFlag != "" { - configName = configNameFlag - } - configPathFlag, err := cmd.Flags().GetString("config-dir") - if err == nil && configPathFlag != "" { - configDirPath = configPathFlag - } - } - if envConfigName := os.Getenv("MANAGER_CONFIG_NAME"); envConfigName != "" { - configName = envConfigName - } - if envConfigPath := os.Getenv("MANAGER_CONFIG_DIR_PATH"); envConfigPath != "" { - configDirPath = envConfigPath - } - return configName, configDirPath -} diff --git a/manager/app/module.go b/manager/app/module.go index c2371cb..5aac5eb 100644 --- a/manager/app/module.go +++ b/manager/app/module.go @@ -65,8 +65,9 @@ func RepoModule(cfg config.ManageConfig) (fx.Option, error) { } // ServiceModule creates an Fx module that provides the service layer, return domain.Service -func ServiceModule(repoModule fx.Option) (fx.Option, error) { +func ServiceModule(adapterModule, repoModule fx.Option) (fx.Option, error) { return fx.Options( + adapterModule, repoModule, fx.Provide(service.NewService), ), nil diff --git a/manager/app/rest_app.go b/manager/app/rest_app.go index 3dc2f59..1f76872 100644 --- a/manager/app/rest_app.go +++ b/manager/app/rest_app.go @@ -2,18 +2,16 @@ package app import ( "context" - "os" "github.com/Gthulhu/api/config" + "github.com/Gthulhu/api/manager/migration" "github.com/Gthulhu/api/manager/rest" "github.com/Gthulhu/api/pkg/logger" "github.com/labstack/echo/v4" - "github.com/spf13/cobra" "go.uber.org/fx" ) func NewRestApp(configName string, configDirPath string) (*fx.App, error) { - cfg, err := config.InitManagerConfig(configName, configDirPath) if err != nil { return nil, err @@ -24,7 +22,12 @@ func NewRestApp(configName string, configDirPath string) (*fx.App, error) { return nil, err } - serviceModule, err := ServiceModule(repoModule) + adapterModule, err := AdapterModule() + if err != nil { + return nil, err + } + + serviceModule, err := ServiceModule(adapterModule, repoModule) if err != nil { return nil, err } @@ -36,6 +39,7 @@ func NewRestApp(configName string, configDirPath string) (*fx.App, error) { app := fx.New( handlerModule, + fx.Invoke(migration.RunMongoMigration), fx.Invoke(StartRestApp), ) return app, nil @@ -56,7 +60,7 @@ func StartRestApp(lc fx.Lifecycle, cfg config.ServerConfig, handler *rest.Handle go func() { logger.Logger(ctx).Info().Msgf("starting rest server on port %s", serverHost) if err := engine.Start(serverHost); err != nil { - logger.Logger(ctx).Fatal().Msgf("start rest server fail on port %s", serverHost) + logger.Logger(ctx).Fatal().Err(err).Msgf("start rest server fail on port %s", serverHost) } }() return nil @@ -69,25 +73,3 @@ func StartRestApp(lc fx.Lifecycle, cfg config.ServerConfig, handler *rest.Handle return nil } - -func getConfigInfo(cmd *cobra.Command) (string, string) { - configName := "manager_config" - configDirPath := "" - if cmd != nil { - configNameFlag, err := cmd.Flags().GetString("config-name") - if err == nil && configNameFlag != "" { - configName = configNameFlag - } - configPathFlag, err := cmd.Flags().GetString("config-dir") - if err == nil && configPathFlag != "" { - configDirPath = configPathFlag - } - } - if envConfigName := os.Getenv("MANAGER_CONFIG_NAME"); envConfigName != "" { - configName = envConfigName - } - if envConfigPath := os.Getenv("MANAGER_CONFIG_DIR_PATH"); envConfigPath != "" { - configDirPath = envConfigPath - } - return configName, configDirPath -} diff --git a/manager/client/deicison_maker.go b/manager/client/deicison_maker.go index b243e7b..6b4e749 100644 --- a/manager/client/deicison_maker.go +++ b/manager/client/deicison_maker.go @@ -1,22 +1,66 @@ package client import ( + "bytes" "context" - "errors" + "encoding/json" + "fmt" "net/http" + "strconv" + dmrest "github.com/Gthulhu/api/decisionmaker/rest" "github.com/Gthulhu/api/manager/domain" + "github.com/Gthulhu/api/pkg/logger" ) func NewDecisionMakerClient() domain.DecisionMakerAdapter { - return &DecisionMakerClient{} + return &DecisionMakerClient{ + Client: http.DefaultClient, + } } type DecisionMakerClient struct { - http.Client + *http.Client } func (dm DecisionMakerClient) SendSchedulingIntent(ctx context.Context, decisionMaker *domain.DecisionMakerPod, intents []*domain.ScheduleIntent) error { - // TODO: Implementation of sending scheduling intents to the decision maker pod - return errors.New("not implemented") + logger.Logger(ctx).Debug().Msgf("Sending %d scheduling intents to decision maker pod (host:%s nodeID:%s port:%d)", len(intents), decisionMaker.Host, decisionMaker.NodeID, decisionMaker.Port) + + reqPayload := dmrest.HandleIntentsRequest{ + Intents: make([]dmrest.Intent, 0, len(intents)), + } + for _, intent := range intents { + reqPayload.Intents = append(reqPayload.Intents, dmrest.Intent{ + PodName: intent.PodName, + PodID: intent.PodID, + NodeID: intent.NodeID, + K8sNamespace: intent.K8sNamespace, + CommandRegex: intent.CommandRegex, + Priority: intent.Priority, + ExecutionTime: intent.ExecutionTime, + PodLabels: intent.PodLabels, + }) + } + + jsonBody, err := json.Marshal(reqPayload) + if err != nil { + return err + } + endpoint := "http://" + decisionMaker.Host + ":" + strconv.Itoa(decisionMaker.Port) + "/api/v1/intents" + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(jsonBody)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + // TODO: add authentication headers if needed + + resp, err := dm.Client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("decision maker %s returned non-OK status: %s", decisionMaker, resp.Status) + } + return nil } diff --git a/manager/cmd/cmd.go b/manager/cmd/cmd.go new file mode 100644 index 0000000..4fe7d30 --- /dev/null +++ b/manager/cmd/cmd.go @@ -0,0 +1,73 @@ +package cmd + +import ( + "context" + "os" + + managerapp "github.com/Gthulhu/api/manager/app" + "github.com/Gthulhu/api/pkg/logger" + "github.com/spf13/cobra" +) + +func init() { + ManagerCmd.Flags().StringP("config-name", "c", "", "Configuration file name without extension") + ManagerCmd.Flags().StringP("config-dir", "d", "", "Configuration file directory path") +} + +// @title manager service +// @version 1.0 +// @description manager service API documentation +// @termsOfService http://swagger.io/terms/ + +// @contact.name API Support + +// @host localhost:8080 +// @BasePath / + +// @Accept json +// @Produce json + +// @securityDefinitions.apikey BearerAuth +// @in header +// @name Authorization + +// @schemes http + +// @externalDocs.description OpenAPI +// @externalDocs.url https://swagger.io/resources/open-api/ +func RunManagerApp(cmd *cobra.Command, args []string) { + configName, configDirPath := getConfigInfo(cmd) + logger.InitLogger() + app, err := managerapp.NewRestApp(configName, configDirPath) + if err != nil { + logger.Logger(context.Background()).Fatal().Err(err).Msg("failed to create rest app") + } + app.Run() +} + +func getConfigInfo(cmd *cobra.Command) (string, string) { + configName := "manager_config" + configDirPath := "" + if cmd != nil { + configNameFlag, err := cmd.Flags().GetString("config-name") + if err == nil && configNameFlag != "" { + configName = configNameFlag + } + configPathFlag, err := cmd.Flags().GetString("config-dir") + if err == nil && configPathFlag != "" { + configDirPath = configPathFlag + } + } + if envConfigName := os.Getenv("MANAGER_CONFIG_NAME"); envConfigName != "" { + configName = envConfigName + } + if envConfigPath := os.Getenv("MANAGER_CONFIG_DIR_PATH"); envConfigPath != "" { + configDirPath = envConfigPath + } + return configName, configDirPath +} + +var ManagerCmd = &cobra.Command{ + Run: RunManagerApp, + Use: "manager", +} diff --git a/manager/domain/k8s_resource.go b/manager/domain/k8s_resource.go index 1951ca6..f2f9722 100644 --- a/manager/domain/k8s_resource.go +++ b/manager/domain/k8s_resource.go @@ -7,7 +7,12 @@ type DecisionMakerPod struct { State NodeState } +func (d *DecisionMakerPod) String() string { + return "(" + d.NodeID + ")" + d.Host + ":" + string(rune(d.Port)) +} + type Pod struct { + Name string K8SNamespace string Labels map[string]string PodID string diff --git a/manager/domain/strategy.go b/manager/domain/strategy.go index 0978dc2..1599641 100644 --- a/manager/domain/strategy.go +++ b/manager/domain/strategy.go @@ -27,6 +27,7 @@ func NewScheduleIntent(strategy *ScheduleStrategy, pod *Pod) ScheduleIntent { ExecutionTime: strategy.ExecutionTime, PodLabels: pod.Labels, State: IntentStateInitialized, + PodName: pod.Name, } } @@ -34,6 +35,7 @@ type ScheduleIntent struct { BaseEntity `bson:",inline"` StrategyID bson.ObjectID `bson:"strategyID,omitempty"` PodID string `bson:"podID,omitempty"` + PodName string `bson:"podName,omitempty"` NodeID string `bson:"nodeID,omitempty"` K8sNamespace string `bson:"k8sNamespace,omitempty"` CommandRegex string `bson:"commandRegex,omitempty"` diff --git a/manager/k8s_adapter/adapter.go b/manager/k8s_adapter/adapter.go index 1c33ee4..f26b9bf 100644 --- a/manager/k8s_adapter/adapter.go +++ b/manager/k8s_adapter/adapter.go @@ -10,6 +10,7 @@ import ( "time" "github.com/Gthulhu/api/manager/domain" + "github.com/Gthulhu/api/pkg/logger" apiv1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -96,6 +97,7 @@ func (a *Adapter) startPodWatcher() { if !ok { return } + logger.Logger(context.Background()).Debug().Msgf("pod added: %s/%s", pod.Namespace, pod.Name) a.setPodCache(*pod) }, UpdateFunc: func(_, newObj interface{}) { @@ -103,11 +105,13 @@ func (a *Adapter) startPodWatcher() { if !ok { return } + logger.Logger(context.Background()).Debug().Msgf("pod updated: %s/%s", pod.Namespace, pod.Name) a.setPodCache(*pod) }, DeleteFunc: func(obj interface{}) { switch pod := obj.(type) { case *apiv1.Pod: + logger.Logger(context.Background()).Debug().Msgf("pod deleted: %s/%s", pod.Namespace, pod.Name) a.deletePodCache(string(pod.UID)) case cache.DeletedFinalStateUnknown: if p, ok := pod.Obj.(*apiv1.Pod); ok { @@ -121,6 +125,7 @@ func (a *Adapter) startPodWatcher() { synced := cache.WaitForCacheSync(a.stopCh, podInformer.HasSynced) a.cacheHasSynced.Store(synced) + logger.Logger(context.Background()).Info().Msg("starting k8s pod watcher") }) } @@ -169,6 +174,7 @@ func (a *Adapter) QueryPods(ctx context.Context, opt *domain.QueryPodsOptions) ( podLabels := copyLabels(pod.Labels) result := &domain.Pod{ + Name: pod.Name, K8SNamespace: pod.Namespace, Labels: podLabels, PodID: string(pod.UID), diff --git a/manager/k8s_adapter/adapter_local_test.go b/manager/k8s_adapter/adapter_local_test.go index 6380ca3..5744d9a 100644 --- a/manager/k8s_adapter/adapter_local_test.go +++ b/manager/k8s_adapter/adapter_local_test.go @@ -1,3 +1,6 @@ +//go:build k3d +// +build k3d + /* For local testing, use `k3d`. diff --git a/manager/k8s_adapter/adapter_test.go b/manager/k8s_adapter/adapter_test.go index 20a4bd1..e292b63 100644 --- a/manager/k8s_adapter/adapter_test.go +++ b/manager/k8s_adapter/adapter_test.go @@ -1,3 +1,6 @@ +//go:build k3d +// +build k3d + package k8sadapter import ( diff --git a/manager/migration/tool.go b/manager/migration/tool.go index 14d255b..3f2306c 100644 --- a/manager/migration/tool.go +++ b/manager/migration/tool.go @@ -24,7 +24,9 @@ func RunMongoMigration(mongodbCfg config.MongoDBConfig) error { dbName := mongodbCfg.Database mongoOpts := options.Client().ApplyURI(uri) - if mongodbCfg.CAPem != "" { + hasCa := false + if mongodbCfg.CAPem != "" && mongodbCfg.CAPemEnable { + hasCa = true caPool := x509.NewCertPool() caPool.AppendCertsFromPEM([]byte(mongodbCfg.CAPem)) tlsConfig := &tls.Config{ @@ -36,7 +38,11 @@ func RunMongoMigration(mongodbCfg config.MongoDBConfig) error { } client, err := mongo.Connect(ctx, mongoOpts) if err != nil { - return fmt.Errorf("connect to mongodb: %w, uri:%s", err, uri) + return fmt.Errorf("connect to mongodb: %w, uri:%s hasCa:%+v", err, uri, hasCa) + } + err = client.Ping(ctx, nil) + if err != nil { + return fmt.Errorf("ping mongodb: %w, uri:%s, hasCa:%+v", err, uri, hasCa) } driverConfig := &mongodbmigrate.Config{ diff --git a/manager/repository/repo.go b/manager/repository/repo.go index 155c819..a4f6cae 100644 --- a/manager/repository/repo.go +++ b/manager/repository/repo.go @@ -26,7 +26,7 @@ func NewRepository(params Params) (domain.Repository, error) { uri := params.MongoConfig.GetURI() mongoOpts := options.Client().ApplyURI(uri) - if params.MongoConfig.CAPem != "" { + if params.MongoConfig.CAPem != "" && params.MongoConfig.CAPemEnable { caPool := x509.NewCertPool() caPool.AppendCertsFromPEM([]byte(params.MongoConfig.CAPem)) tlsConfig := &tls.Config{ @@ -39,11 +39,11 @@ func NewRepository(params Params) (domain.Repository, error) { client, err := mongo.Connect(mongoOpts) if err != nil { - return nil, fmt.Errorf("connect to mongodb: %w, uri:%s", err, uri) + return nil, fmt.Errorf("connect to mongodb: %w, uri:%s, tls:%+v", err, uri, params.MongoConfig.CAPemEnable) } if err := client.Ping(ctx, nil); err != nil { - return nil, fmt.Errorf("ping mongodb: %w, uri:%s", err, uri) + return nil, fmt.Errorf("ping mongodb: %w, uri:%s, tls:%+v", err, uri, params.MongoConfig.CAPemEnable) } dbName := params.MongoConfig.Database diff --git a/manager/rest/auth_hdl_test.go b/manager/rest/auth_hdl_test.go index ebed687..75c0877 100644 --- a/manager/rest/auth_hdl_test.go +++ b/manager/rest/auth_hdl_test.go @@ -10,7 +10,7 @@ import ( ) func (suite *HandlerTestSuite) TestIntegrationAuthHandler() { - adminUser, adminPwd := config.GetConfig().Account.AdminEmail, config.GetConfig().Account.AdminPassword + adminUser, adminPwd := config.GetManagerConfig().Account.AdminEmail, config.GetManagerConfig().Account.AdminPassword adminToken := suite.login(adminUser, adminPwd.Value(), http.StatusOK) // Test creating a new user diff --git a/manager/rest/handler_test.go b/manager/rest/handler_test.go index 2331ac9..408e9a8 100644 --- a/manager/rest/handler_test.go +++ b/manager/rest/handler_test.go @@ -60,18 +60,21 @@ func (suite *HandlerTestSuite) SetupSuite() { repoModule, err := app.TestRepoModule(cfg, suite.ContainerBuilder) suite.Require().NoError(err, "Failed to create repo module") - serviceModule, err := app.ServiceModule(repoModule) - suite.Require().NoError(err, "Failed to create service module") - - handlerModule, err := app.HandlerModule(serviceModule) - suite.Require().NoError(err, "Failed to create handler module") - opt := fx.Options( + adapterModule := fx.Options( fx.Provide(func() domain.K8SAdapter { return suite.MockK8SAdapter }), fx.Provide(func() domain.DecisionMakerAdapter { return suite.MockDMAdapter }), + ) + + serviceModule, err := app.ServiceModule(adapterModule, repoModule) + suite.Require().NoError(err, "Failed to create service module") + + handlerModule, err := app.HandlerModule(serviceModule) + suite.Require().NoError(err, "Failed to create handler module") + opt := fx.Options( handlerModule, fx.Invoke(migration.RunMongoMigration), fx.Populate(&suite.Handler), @@ -96,7 +99,7 @@ func (suite *HandlerTestSuite) SetupTest() { suite.Require().NoError(err, "Failed to clean up MongoDB") err = migration.RunMongoMigration(suite.mongoDBCfg) suite.Require().NoError(err, "Failed to run MongoDB migrations") - err = suite.Handler.Svc.CreateAdminUserIfNotExists(suite.Ctx, config.GetConfig().Account.AdminEmail, config.GetConfig().Account.AdminPassword.Value()) + err = suite.Handler.Svc.CreateAdminUserIfNotExists(suite.Ctx, config.GetManagerConfig().Account.AdminEmail, config.GetManagerConfig().Account.AdminPassword.Value()) suite.Require().NoError(err, "Failed to create admin user") } diff --git a/manager/rest/role_hdl_test.go b/manager/rest/role_hdl_test.go index 81e25a4..f7e4028 100644 --- a/manager/rest/role_hdl_test.go +++ b/manager/rest/role_hdl_test.go @@ -10,7 +10,7 @@ import ( ) func (suite *HandlerTestSuite) TestIntegrationRoleHandler() { - adminUser, adminPwd := config.GetConfig().Account.AdminEmail, config.GetConfig().Account.AdminPassword + adminUser, adminPwd := config.GetManagerConfig().Account.AdminEmail, config.GetManagerConfig().Account.AdminPassword adminToken := suite.login(adminUser, adminPwd.Value(), http.StatusOK) roleManager := "role_manager" diff --git a/manager/rest/routes.go b/manager/rest/routes.go index aa96eee..82f1fc5 100644 --- a/manager/rest/routes.go +++ b/manager/rest/routes.go @@ -3,7 +3,7 @@ package rest import ( "net/http" - "github.com/Gthulhu/api/docs" + docs "github.com/Gthulhu/api/docs/manager" "github.com/Gthulhu/api/manager/domain" "github.com/labstack/echo/v4" echoSwagger "github.com/swaggo/echo-swagger" diff --git a/manager/rest/strategy_hdl.go b/manager/rest/strategy_hdl.go index ab02465..07886a9 100644 --- a/manager/rest/strategy_hdl.go +++ b/manager/rest/strategy_hdl.go @@ -21,6 +21,20 @@ type CreateScheduleStrategyRequest struct { ExecutionTime int64 `json:"executionTime,omitempty"` } +// CreateScheduleStrategy godoc +// @Summary Create schedule strategy +// @Description Create a new schedule strategy. +// @Tags Strategies +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body CreateScheduleStrategyRequest true "Schedule strategy payload" +// @Success 200 {object} SuccessResponse[EmptyResponse] +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /api/v1/strategies [post] func (h *Handler) CreateScheduleStrategy(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req CreateScheduleStrategyRequest @@ -75,6 +89,19 @@ type ScheduleStrategy struct { ExecutionTime int64 `bson:"executionTime,omitempty"` } +// ListSelfScheduleStrategies godoc +// @Summary List self schedule strategies +// @Description List schedule strategies created by the authenticated user. +// @Tags Strategies +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} SuccessResponse[ListSchedulerStrategiesResponse] +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /api/v1/strategies/self [get] func (h *Handler) ListSelfScheduleStrategies(w http.ResponseWriter, r *http.Request) { ctx := r.Context() claims, ok := h.GetClaimsFromContext(ctx) @@ -148,6 +175,19 @@ type ScheduleIntent struct { State domain.IntentState `bson:"state,omitempty"` } +// ListSelfScheduleIntents godoc +// @Summary List self schedule intents +// @Description List schedule intents created by the authenticated user. +// @Tags Strategies +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} SuccessResponse[ListScheduleIntentsResponse] +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /api/v1/intents/self [get] func (h *Handler) ListSelfScheduleIntents(w http.ResponseWriter, r *http.Request) { ctx := r.Context() claims, ok := h.GetClaimsFromContext(ctx) diff --git a/manager/rest/strategy_hdl_test.go b/manager/rest/strategy_hdl_test.go index 1d9aa5b..ef1ee2b 100644 --- a/manager/rest/strategy_hdl_test.go +++ b/manager/rest/strategy_hdl_test.go @@ -10,7 +10,7 @@ import ( ) func (suite *HandlerTestSuite) TestIntegrationStrategyHandler() { - adminUser, adminPwd := config.GetConfig().Account.AdminEmail, config.GetConfig().Account.AdminPassword + adminUser, adminPwd := config.GetManagerConfig().Account.AdminEmail, config.GetManagerConfig().Account.AdminPassword adminToken := suite.login(adminUser, adminPwd.Value(), http.StatusOK) strategyReq := rest.CreateScheduleStrategyRequest{ diff --git a/manager/service/strategy_svc.go b/manager/service/strategy_svc.go index efa4f87..32cfa1d 100644 --- a/manager/service/strategy_svc.go +++ b/manager/service/strategy_svc.go @@ -52,8 +52,8 @@ func (svc *Service) CreateScheduleStrategy(ctx context.Context, operator *domain } dmLabel := domain.LabelSelector{ - Key: "role", - Value: "decision-maker", + Key: "app", + Value: "decisionmaker", } dmQueryOpt := &domain.QueryDecisionMakerPodsOptions{ diff --git a/pkg/middleware/logger.go b/pkg/middleware/logger.go new file mode 100644 index 0000000..6b13553 --- /dev/null +++ b/pkg/middleware/logger.go @@ -0,0 +1,79 @@ +package middleware + +import ( + "bytes" + "net/http" + "runtime/debug" + "time" + + "github.com/Gthulhu/api/pkg/logger" + "github.com/rs/xid" +) + +func LoggerMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + reqID := r.Header.Get("X-Request-ID") + if reqID == "" { + reqID = xid.New().String() + } + start := time.Now() + log := logger.Logger(ctx).With(). + Str("method", r.Method).Str("req_id", reqID). + Str("url", r.URL.String()).Logger() + + defer func() { + if err := recover(); err != nil { + log.Error().Interface("panic", err).Msgf("Recovered from panic, stack trace: %s", string(debug.Stack())) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } + }() + + ctx = log.WithContext(ctx) + r = r.WithContext(ctx) + responseWriter := NewResponseWriter(w) + next.ServeHTTP(responseWriter, r) + cost := time.Since(start) + log = log.With(). + Int("cost_msec", int(cost.Milliseconds())). + Logger() + if responseWriter.statusCode >= 500 { + log.Error(). + Int("status_code", responseWriter.statusCode). + Str("response_body", responseWriter.responseBody.String()). + Msg("Request completed with server error") + } else if responseWriter.statusCode >= 400 { + log.Warn(). + Int("status_code", responseWriter.statusCode). + Str("response_body", responseWriter.responseBody.String()). + Msg("Request completed with client error") + } else { + log.Info(). + Int("status_code", responseWriter.statusCode). + Msg("Request completed successfully") + } + }) +} + +type responseWriter struct { + http.ResponseWriter + responseBody bytes.Buffer + statusCode int +} + +func NewResponseWriter(w http.ResponseWriter) *responseWriter { + return &responseWriter{ + ResponseWriter: w, + } +} + +func (rw *responseWriter) WriteHeader(code int) { + rw.statusCode = code + rw.ResponseWriter.WriteHeader(code) +} + +func (rw *responseWriter) Write(b []byte) (int, error) { + rw.responseBody.Write(b) + return rw.ResponseWriter.Write(b) +} From d7e83898d8c4a90ec8609df5d1fd37d5e1b18d11 Mon Sep 17 00:00:00 2001 From: Vic Xu Date: Sat, 20 Dec 2025 07:03:33 +0800 Subject: [PATCH 17/18] refactor: add polling scheduling strategies api to decision maker server --- decisionmaker/domain/pod.go | 13 +++ decisionmaker/rest/handler.go | 47 +---------- decisionmaker/rest/intent_handler.go | 113 +++++++++++++++++++++++++++ decisionmaker/service/service.go | 46 +++++++++-- deployment/kind/local_test.sh | 8 ++ pkg/util/generic_map.go | 92 ++++++++++++++++++++++ 6 files changed, 269 insertions(+), 50 deletions(-) create mode 100644 decisionmaker/rest/intent_handler.go create mode 100644 deployment/kind/local_test.sh create mode 100644 pkg/util/generic_map.go diff --git a/decisionmaker/domain/pod.go b/decisionmaker/domain/pod.go index 19c3fc2..0296a8e 100644 --- a/decisionmaker/domain/pod.go +++ b/decisionmaker/domain/pod.go @@ -25,3 +25,16 @@ type Intent struct { ExecutionTime int64 `json:"executionTime,omitempty"` PodLabels map[string]string `json:"podLabels,omitempty"` } + +type SchedulingIntents struct { + Priority bool `json:"priority"` // If true, set vtime to minimum vtime + ExecutionTime uint64 `json:"execution_time"` // Time slice for this process in nanoseconds + PID int `json:"pid,omitempty"` // Process ID to apply this strategy to + Selectors []LabelSelector `json:"selectors,omitempty"` // Label selectors to match pods + CommandRegex string `json:"command_regex,omitempty"` // Regex to match process command +} + +type LabelSelector struct { + Key string `json:"key"` + Value string `json:"value"` +} diff --git a/decisionmaker/rest/handler.go b/decisionmaker/rest/handler.go index b60a0c9..c2522af 100644 --- a/decisionmaker/rest/handler.go +++ b/decisionmaker/rest/handler.go @@ -6,7 +6,6 @@ import ( "net/http" "time" - "github.com/Gthulhu/api/decisionmaker/domain" "github.com/Gthulhu/api/decisionmaker/service" "github.com/Gthulhu/api/manager/errs" "github.com/Gthulhu/api/pkg/logger" @@ -122,7 +121,7 @@ func (h *Handler) Version(w http.ResponseWriter, r *http.Request) { response := VersionResponse{ Message: "BSS Metrics API Server", Version: "1.0.0", - Endpoints: "/api/v1/auth/token (POST), /api/v1/metrics (POST), /api/v1/pods/pids (GET), /api/v1/scheduling/strategies (GET, POST), /health (GET), /static/ (Frontend)", + Endpoints: "/health, /version, POST_/api/v1/intents, GET_/api/v1/scheduling/strategies", } h.JSONResponse(r.Context(), w, http.StatusOK, response) } @@ -143,52 +142,9 @@ func (h *Handler) HealthCheck(w http.ResponseWriter, r *http.Request) { h.JSONResponse(r.Context(), w, http.StatusOK, response) } -type HandleIntentsRequest struct { - Intents []Intent `json:"intents"` -} - -type Intent struct { - PodName string `json:"podName,omitempty"` - PodID string `json:"podID,omitempty"` - NodeID string `json:"nodeID,omitempty"` - K8sNamespace string `json:"k8sNamespace,omitempty"` - CommandRegex string `json:"commandRegex,omitempty"` - Priority int `json:"priority,omitempty"` - ExecutionTime int64 `json:"executionTime,omitempty"` - PodLabels map[string]string `json:"podLabels,omitempty"` -} - -func (h *Handler) HandleIntents(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - var req HandleIntentsRequest - err := h.JSONBind(r, &req) - if err != nil { - h.ErrorResponse(ctx, w, http.StatusBadRequest, "Invalid request payload", err) - return - } - intents := make([]*domain.Intent, 0, len(req.Intents)) - for _, intent := range req.Intents { - intents = append(intents, &domain.Intent{ - PodName: intent.PodName, - PodID: intent.PodID, - NodeID: intent.NodeID, - K8sNamespace: intent.K8sNamespace, - CommandRegex: intent.CommandRegex, - Priority: intent.Priority, - ExecutionTime: intent.ExecutionTime, - PodLabels: intent.PodLabels, - }) - } - // TODO: forward intents to the ebpf user space agent - h.Service.ProcessIntents(r.Context(), intents) - h.JSONResponse(ctx, w, http.StatusOK, NewSuccessResponse[EmptyResponse](nil)) -} - func (h *Handler) SetupRoutes(engine *echo.Echo) { engine.GET("/health", h.echoHandler(h.HealthCheck)) engine.GET("/version", h.echoHandler(h.Version)) - // docs.SwaggerInfo.BasePath = "/" - // engine.GET("/swagger/*", echoSwagger.WrapHandler) api := engine.Group("/api", echo.WrapMiddleware(middleware.LoggerMiddleware)) // v1 routes @@ -196,6 +152,7 @@ func (h *Handler) SetupRoutes(engine *echo.Echo) { apiV1 := api.Group("/v1") // auth routes apiV1.POST("/intents", h.echoHandler(h.HandleIntents)) + apiV1.GET("/scheduling/strategies", h.echoHandler(h.ListIntents)) } } diff --git a/decisionmaker/rest/intent_handler.go b/decisionmaker/rest/intent_handler.go new file mode 100644 index 0000000..179134a --- /dev/null +++ b/decisionmaker/rest/intent_handler.go @@ -0,0 +1,113 @@ +package rest + +import ( + "net/http" + "time" + + "github.com/Gthulhu/api/decisionmaker/domain" +) + +type HandleIntentsRequest struct { + Intents []Intent `json:"intents"` +} + +type Intent struct { + PodName string `json:"podName,omitempty"` + PodID string `json:"podID,omitempty"` + NodeID string `json:"nodeID,omitempty"` + K8sNamespace string `json:"k8sNamespace,omitempty"` + CommandRegex string `json:"commandRegex,omitempty"` + Priority int `json:"priority,omitempty"` + ExecutionTime int64 `json:"executionTime,omitempty"` + PodLabels map[string]string `json:"podLabels,omitempty"` +} + +func (h *Handler) HandleIntents(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var req HandleIntentsRequest + err := h.JSONBind(r, &req) + if err != nil { + h.ErrorResponse(ctx, w, http.StatusBadRequest, "Invalid request payload", err) + return + } + intents := make([]*domain.Intent, 0, len(req.Intents)) + for _, intent := range req.Intents { + intents = append(intents, &domain.Intent{ + PodName: intent.PodName, + PodID: intent.PodID, + NodeID: intent.NodeID, + K8sNamespace: intent.K8sNamespace, + CommandRegex: intent.CommandRegex, + Priority: intent.Priority, + ExecutionTime: intent.ExecutionTime, + PodLabels: intent.PodLabels, + }) + } + err = h.Service.ProcessIntents(r.Context(), intents) + if err != nil { + h.ErrorResponse(ctx, w, http.StatusInternalServerError, "Failed to process intents", err) + return + } + h.JSONResponse(ctx, w, http.StatusOK, NewSuccessResponse[EmptyResponse](nil)) +} + +// SchedulingStrategy represents a strategy for process scheduling +type SchedulingIntents struct { + Priority bool `json:"priority"` // If true, set vtime to minimum vtime + ExecutionTime uint64 `json:"execution_time"` // Time slice for this process in nanoseconds + PID int `json:"pid,omitempty"` // Process ID to apply this strategy to + Selectors []LabelSelector `json:"selectors,omitempty"` // Label selectors to match pods + CommandRegex string `json:"command_regex,omitempty"` // Regex to match process command +} + +// LabelSelector represents a key-value pair for pod label selection +type LabelSelector struct { + Key string `json:"key"` // Label key + Value string `json:"value"` // Label value +} + +type ListIntentsResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Timestamp string `json:"timestamp"` + Scheduling []*SchedulingIntents `json:"scheduling"` +} + +func (h *Handler) ListIntents(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + intents, err := h.Service.ListAllSchedulingIntents(ctx) + if err != nil { + h.ErrorResponse(ctx, w, http.StatusInternalServerError, "Failed to list scheduling intents", err) + return + } + + schedulingIntents := make([]*SchedulingIntents, 0, len(intents)) + for _, intent := range intents { + schedulingIntents = append(schedulingIntents, &SchedulingIntents{ + Priority: intent.Priority, + ExecutionTime: intent.ExecutionTime, + PID: intent.PID, + Selectors: convertMapToLabelSelectors(intent.Selectors), + CommandRegex: intent.CommandRegex, + }) + } + + response := ListIntentsResponse{ + Success: true, + Message: "Scheduling intents retrieved successfully", + Timestamp: time.Now().UTC().Format(time.RFC3339), // You can set the current timestamp here if needed + Scheduling: schedulingIntents, + } + h.JSONResponse(ctx, w, http.StatusOK, response) +} + +func convertMapToLabelSelectors(selectorMap []domain.LabelSelector) []LabelSelector { + labelSelectors := make([]LabelSelector, 0, len(selectorMap)) + for _, sel := range selectorMap { + labelSelectors = append(labelSelectors, LabelSelector{ + Key: sel.Key, + Value: sel.Value, + }) + } + return labelSelectors +} diff --git a/decisionmaker/service/service.go b/decisionmaker/service/service.go index fa63d0e..4c17401 100644 --- a/decisionmaker/service/service.go +++ b/decisionmaker/service/service.go @@ -11,21 +11,36 @@ import ( "github.com/Gthulhu/api/decisionmaker/domain" "github.com/Gthulhu/api/pkg/logger" + "github.com/Gthulhu/api/pkg/util" ) func NewService() Service { - return Service{} + return Service{ + schedulingIntentsMap: util.NewGenericMap[string, []*domain.SchedulingIntents](), + } } type Service struct { + schedulingIntentsMap *util.GenericMap[string, []*domain.SchedulingIntents] } const ( - procDir = "/proc" + procDir = "/proc" + pauseCommand = "pause" ) +// ListAllSchedulingIntents retrieves all stored scheduling intents +func (svc *Service) ListAllSchedulingIntents(ctx context.Context) ([]*domain.SchedulingIntents, error) { + intents := []*domain.SchedulingIntents{} + svc.schedulingIntentsMap.Range(func(key string, value []*domain.SchedulingIntents) bool { + intents = append(intents, value...) + return true + }) + return intents, nil +} + +// ProcessIntents processes a list of scheduling intents and updates the internal map func (svc *Service) ProcessIntents(ctx context.Context, intents []*domain.Intent) error { - // Placeholder for processing intents podInfos, err := svc.GetAllPodInfos(ctx) if err != nil { return err @@ -33,8 +48,29 @@ func (svc *Service) ProcessIntents(ctx context.Context, intents []*domain.Intent for _, intent := range intents { podInfo := podInfos[intent.PodID] logger.Logger(ctx).Info().Msgf("Processing intent for PodName:%s PodID: %s on NodeID: %s, Process:%+v", intent.PodName, intent.PodID, intent.NodeID, podInfo) - // Add logic to handle the intent - + labels := []domain.LabelSelector{} + for key, value := range intent.PodLabels { + labels = append(labels, domain.LabelSelector{ + Key: key, + Value: value, + }) + } + if podInfo != nil && len(podInfo.Processes) > 0 { + for _, process := range podInfo.Processes { + if process.Command == pauseCommand { + continue + } + schedulingIntent := &domain.SchedulingIntents{ + Priority: intent.Priority > 0, + ExecutionTime: uint64(intent.ExecutionTime), + PID: process.PID, + CommandRegex: intent.CommandRegex, + Selectors: labels, + } + logger.Logger(ctx).Info().Msgf("Created SchedulingIntent: %+v for Process PID: %d", schedulingIntent, process.PID) + svc.schedulingIntentsMap.Store(fmt.Sprintf("%s-%d", intent.PodID, process.PID), []*domain.SchedulingIntents{schedulingIntent}) + } + } } logger.Logger(ctx).Info().Msgf("Discovered pods: %+v", podInfos) return nil diff --git a/deployment/kind/local_test.sh b/deployment/kind/local_test.sh new file mode 100644 index 0000000..5dedfea --- /dev/null +++ b/deployment/kind/local_test.sh @@ -0,0 +1,8 @@ +curl -X 'POST' \ +'http://localhost:8080/api/v1/auth/login' \ +-H 'accept: application/json' \ +-H 'Content-Type: application/json' \ +-d '{ +"password": "your-password-here", +"username": "admin@example.com" +}' \ No newline at end of file diff --git a/pkg/util/generic_map.go b/pkg/util/generic_map.go new file mode 100644 index 0000000..edc533d --- /dev/null +++ b/pkg/util/generic_map.go @@ -0,0 +1,92 @@ +package util + +import "sync" + +// GenericMap is a concurrent safe map with generic key and value types. +type GenericMap[K comparable, V any] struct { + m sync.Map +} + +// NewGenericMap creates a new instance of GenericMap. +func NewGenericMap[K comparable, V any]() *GenericMap[K, V] { + return &GenericMap[K, V]{} +} + +// Load returns the value stored in the map for a key, or nil if no +// value is present. +// The ok result indicates whether value was found in the map. +func (m *GenericMap[K, V]) Load(key K) (value V, ok bool) { + v, loaded := m.m.Load(key) + if !loaded { + var zero V + return zero, false + } + return v.(V), true +} + +// Store sets the value for a key. +func (m *GenericMap[K, V]) Store(key K, value V) { + m.m.Store(key, value) +} + +// Clear deletes all the entries, resulting in an empty Map. +func (m *GenericMap[K, V]) Clear() { + m.m.Clear() +} + +// LoadOrStore returns the existing value for the key if present. +// Otherwise, it stores and returns the given value. +// The loaded result is true if the value was loaded, false if stored. +func (m *GenericMap[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) { + v, loaded := m.m.LoadOrStore(key, value) + return v.(V), loaded +} + +// LoadAndDelete deletes the value for a key, returning the previous value if any. +// The loaded result reports whether the key was present. +func (m *GenericMap[K, V]) LoadAndDelete(key K) (value V, loaded bool) { + v, loaded := m.m.LoadAndDelete(key) + if !loaded { + var zero V + return zero, false + } + return v.(V), true +} + +// Delete deletes the value for a key. +func (m *GenericMap[K, V]) Delete(key K) { + m.m.Delete(key) +} + +// Swap swaps the value for a key and returns the previous value if any. +// The loaded result reports whether the key was present. +func (m *GenericMap[K, V]) Swap(key K, value V) (previous V, loaded bool) { + v, loaded := m.m.Swap(key, value) + if !loaded { + var zero V + return zero, false + } + return v.(V), true +} + +// CompareAndSwap swaps the old and new values for key +// if the value stored in the map is equal to old. +// The old value must be of a comparable type. +func (m *GenericMap[K, V]) CompareAndSwap(key K, old, new V) (swapped bool) { + return m.m.CompareAndSwap(key, old, new) +} + +// CompareAndDelete deletes the entry for key if its value is equal to old. +// The old value must be of a comparable type. +// +// If there is no current value for key in the map, CompareAndDelete +// returns false (even if the old value is the nil interface value). +func (m *GenericMap[K, V]) CompareAndDelete(key K, old V) (deleted bool) { + return m.m.CompareAndDelete(key, old) +} + +func (m *GenericMap[K, V]) Range(f func(key K, value V) bool) { + m.m.Range(func(k, v any) bool { + return f(k.(K), v.(V)) + }) +} From 268a29464d927ec7ecb2869f1b0bc7e6982d0194 Mon Sep 17 00:00:00 2001 From: Vic Xu Date: Sun, 28 Dec 2025 15:39:57 +0800 Subject: [PATCH 18/18] refactor/api: add auth api and metric to decision maker --- Makefile | 9 +- config/dm_config.default.toml | 57 +++++++++ config/dm_config.go | 6 + config/manager_config.default.toml | 17 +++ config/manager_config.go | 2 + config/manager_config.test.toml | 16 +++ decisionmaker/app/module.go | 7 +- decisionmaker/domain/metric_set.go | 15 +++ decisionmaker/rest/handler.go | 30 ++++- decisionmaker/rest/metric_handler.go | 48 ++++++++ decisionmaker/rest/middleware.go | 107 ++++++++++++++++ decisionmaker/rest/token_handler.go | 40 ++++++ decisionmaker/service/metric_collector.go | 144 ++++++++++++++++++++++ decisionmaker/service/service.go | 32 ++++- decisionmaker/service/token_svc.go | 71 +++++++++++ deployment/kind/local_test.sh | 53 ++++++-- deployment/kind/mongo/statefulset.yaml | 7 ++ go.mod | 12 +- go.sum | 26 +++- manager/app/module.go | 4 +- manager/client/deicison_maker.go | 65 +++++++++- manager/domain/mock_domain.go | 114 +++++++++++++++++ pkg/util/encrypt.go | 59 +++++++++ pkg/util/encrypt_test.go | 69 +++++++++++ pkg/util/os.go | 17 +++ 25 files changed, 993 insertions(+), 34 deletions(-) create mode 100644 decisionmaker/domain/metric_set.go create mode 100644 decisionmaker/rest/metric_handler.go create mode 100644 decisionmaker/rest/middleware.go create mode 100644 decisionmaker/rest/token_handler.go create mode 100644 decisionmaker/service/metric_collector.go create mode 100644 decisionmaker/service/token_svc.go create mode 100644 pkg/util/os.go diff --git a/Makefile b/Makefile index 0b32bcd..6d1af34 100644 --- a/Makefile +++ b/Makefile @@ -114,4 +114,11 @@ local-kind-setup: sh $(CURDIR)/deployment/kind/local_setup.sh local-kind-teardown: - sh $(CURDIR)/deployment/kind/local_teardown.sh \ No newline at end of file + sh $(CURDIR)/deployment/kind/local_teardown.sh + +gen-mock: + @go install github.com/vektra/mockery/v3@v3.5.5 + @mockery + +test-all: + go test ./... -count=1 -p=1 \ No newline at end of file diff --git a/config/dm_config.default.toml b/config/dm_config.default.toml index 322410d..adae6aa 100644 --- a/config/dm_config.default.toml +++ b/config/dm_config.default.toml @@ -5,3 +5,60 @@ host = ":8080" [logging] level = "info" path = "logs/app.log" + +[token] +enable = true +rsa_private_key_pem = """ +-----BEGIN RSA PRIVATE KEY----- +MIIJJwIBAAKCAgEAny28YMC2/+yYj3T29lz60uryNz8gNVrqD7lTJuHQ3DMTE6AD +qnERy8VgHve0tWzhJc5ZBZ1Hduvj+z/kNqbcU81YGhmfOrQ3iFNYBlSAseIHdAw3 +9HGyC6OKzTXI4HRpc8CwcF6hKExkyWlkALr5i+IQDfimvarjjZ6Nm368L0Rthv3K +OkI5CqRZ6bsVwwBug7GcdkvFs3LiRSKlMBpH2tCkZ5ZZE8VyuK7VnlwV7n6EHzN5 +BqaHq8HVLw2KzvibSi+/5wIZV2Yx33tViLbhOsZqLt6qQCGGgKzNX4TGwRLGAiVV +1NCpgQhimZ4YP2thqSsqbaISOuvFlYq+QGP1bcvcHB7UhT1ZnHSDYcbT2qiD3Voq +ytXVKLB1X5XCD99YLSP9B32f1lvZD4MhDtE4IhAuqn15MGB5ct4yj/uMldFScs9K +hqnWcwS4K6Qx3IfdB+ZxT5hEOWJLEcGqe/CSXITNG7oS9mrSAJJvHSLz++4R/Sh1 +MnT2YWjyDk6qeeqAwut0w5iDKWt7qsGEcHFPIVVlos+xLfrPDtgHQk8upjslUcMy +MDTf21Y3RdJ3k1gTR9KHEwzKeiNlLjen9ekFWupF8jik1aYRWL6h54ZyGxwKEyMY +i9o18G2pXPzvVaPYtU+TGXdO4QwiES72TNCDbNaGj75Gj0sN+LfjjQ4A898CAwEA +AQKCAgAFrHuqdzQOq0BE3MZwwZ+vJPC9R2K+hB8TsGdmW2Y2cxua93kp+h3IRaDH +eczXKqpbzp8dtB13/7CApCZeTFROKGObio5CaWoRUecxUpHDxWq+mDDmZacTAyFP +bztZxMx9c8DWQIk+BnsRMtB9tixu7//if5px6EV0JtKlWD8c8DN3PFSY/wNJfdI2 +opSD/t/xkcMh9FF3tACctj9tF4K4KfeyOYmzSrZsHs8+dcnSVnAfLJaDxivP03jl +1HW+Kt5eJpWQhmKg2uOsM5k45kvg7HGcehNXddp1e7NWVEVBXInySaJlk4p3LvVU +xG3Y1NsGTKOWhNBhiUXhrrBZWzbERLvE7/OtrHVDAlEuA47rFb/rTnMWHIZhNeXt +Hwa7G/11dlwYN3jn5u+/2SkVC0R4X/lqTzFRzCYIWr9lTeVpC53Gn1jmX2PSNDbF +yLi7ZZFBhS8GdNimeKyJReKV2o2nsO49KngGIs5/B318GA5BwBVPf9fVd9a2n29s +ioUXmTE07bpbyXk1BL4jZFOAJYqXh6IOwAFKFtQXgfidod7KcwpxQAJpE1Od3EPd +sGlTTC+hsUzAdlV82wqc6AB80DcxlzsI0adTkD7NrIcRSQtCC9DnZPXm/kFiam80 +gW2SmIsaLYauoQgIcbI1Lpy3rMCMbbTeG7K3KGyYZULFXHRsAQKCAQEAy5n5Xt8z +VaCS+V0VrwpIdPAOzgaxJkcfy3hVLe5vkhaeu5DzyHgW0fPud0P2J1eUD8GWxXvj +6EImFsFydH+DR8ClhHX8awpEn4mS6vR6VVPseqKWzs6BP7usH/WNTIeGJR2z0hRG +ZgD/z4W1PwwL6tIno/oHY3MkflX5/1X6q7vGjNjN7d81Mb48dXcLEjaZWtcRqzy0 +MNXrgvrpe/pCRUeUBV6WYHSPn6OPJA7RgcKegn9AwWEoecieFnjiVSjdfI2WT+Wa +13MCwPoWs1u+mHNl+yaqFnj9u9tF2xF6M8ZERscmvU+k0aDNcuJq/drP6qH/93gn +e7Tjwlf3IqhcLwKCAQEAyCUFaRimDfn43qLBDZpgmBHwCpBQFaNS8jvVQpUlQ4zL +W/pCqMIePajE5E9EBcQZyd+tE6WE202Z2CBzvfyH54HGmAiEE5koQSE6GBZbXrzu +4ToolPR6nrlEDR6ayjuv9BOPR91OZL9EfSgi5kdNIEilnDm1n/VhEIW5y6TsJUGv +VeuTDgnRYbkIVBBppA5U4rYyOz7ES+0L344k05i9LzFvcgc1QX50Dg8dqiI2tm6z +uyjxhJ9TW6R1iqzLnDB01YrDuWN62qISHnNz4X5Z/EBoITo3Og2IMaCeE5yKHCjw +FrrV05F8Cf7B5DLBOTWiX3oPtu9oV7QrjN6WARGHUQKCAQARrH8KLkPthe/cN6lf +NXxOslwGpGwST5BCAGMchpsmylHjJFUVLN+GQC+OKNcgWSjgKUTmRbfl/IAD76z4 +0ezaeK2ljvxnak/ErZOUU76e05cumhiPQTvVBXyOlak7YHRTmn12mg32YtXR9OBj +5a7PJokMYfLsPh2H3fzCnnsRF07IATX3FS4v8DydUcUjQpwTV6IQBEf8CUXVa+SC +v5mrG+iMgsZ4/wVMrU0Kq0KiiftqhpNfdgimcbTPbJTxIYgAfOX0b5D+bNxrVgpM +bYVhBHtwzs1q//u+p+0rdBvwjKB2qGkDe/tpuxS6iU8SVEFCM+fdWo/K3Ev9Hde1 +KXo/AoIBABUAdYHiuUIMMgZCs9lWkr5CW5rwK8cpfUG375fuCJv/ATPknewRepTj +yc1fV/b27fHWC9Zc7wUILpWUSjDsd+JeJtW7Rwi7cJLtBqiSaAIX90UhEjMXOGrB +bBeoV3vTKZKGHunemiROQcSUWp0pbDlwBhjPoXRojkfqkGWDJ9h8/QYaEzNM6nDD +ttEDa+JwMo4bqke3PWfuNum9g7XEeE2kdVpU0UzPFSSIh4db0bvw/+Eq2bUd9uRN +7JuhqDf6ibgCuKkSfEjG6vnRCZ7m4FBs/cBG2Ja55sm2XgAW1BNCZHcuIdPylz6B +Qh1NCiOTsjcsmsuKcbuKR2ufy8PO8BECggEACr4AaasrHcqqBcGCZfGlO4Q0Bbzu +1IJ6u9X+0U+k36wH9cE8RMJZaFo30bYTSBnxN6YY4M1taG3egIZQNgTRpjEiC8lV +spxkAVwlY6g1ZER7IFXlzhz6wYuDBayRhA/2zBPgtGesfpTd7H24AlJ6qB+mThHs +7ZbETM4KP04uNBBPybwaPIOkUPnQInfIPOAJVHebHp8atyavwrpGmRq958XKMgvz +8oHIdzV+XTMi1+U7eg/ITEpOAPD82fYB4UfKRdA1jraXsJJyTs+QFjc2WXBffgAg +X6m7Mp9nAMhRyXhULslO3trWFbFCa2dbQkDSyBRvsb2HZtztoLVyo1mtUg== +-----END RSA PRIVATE KEY----- +""" +token_duration_hr = 24 \ No newline at end of file diff --git a/config/dm_config.go b/config/dm_config.go index af78f10..a640a1b 100644 --- a/config/dm_config.go +++ b/config/dm_config.go @@ -9,6 +9,7 @@ import ( type DecisionMakerConfig struct { Server ServerConfig `mapstructure:"server"` Logging LoggingConfig `mapstructure:"logging"` + Token TokenConfig `mapstructure:"token"` } var ( @@ -45,3 +46,8 @@ func InitDMConfig(configName string, configPath string) (DecisionMakerConfig, er dmConfig = &cfg return cfg, nil } + +type TokenConfig struct { + RsaPrivateKeyPem SecretValue `mapstructure:"rsa_private_key_pem"` + TokenDurationHr int `mapstructure:"token_duration_hr"` // in hours +} diff --git a/config/manager_config.default.toml b/config/manager_config.default.toml index 625840e..9a0d3db 100644 --- a/config/manager_config.default.toml +++ b/config/manager_config.default.toml @@ -74,6 +74,23 @@ spxkAVwlY6g1ZER7IFXlzhz6wYuDBayRhA/2zBPgtGesfpTd7H24AlJ6qB+mThHs X6m7Mp9nAMhRyXhULslO3trWFbFCa2dbQkDSyBRvsb2HZtztoLVyo1mtUg== -----END RSA PRIVATE KEY----- """ +dm_public_key_pem = """ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAny28YMC2/+yYj3T29lz6 +0uryNz8gNVrqD7lTJuHQ3DMTE6ADqnERy8VgHve0tWzhJc5ZBZ1Hduvj+z/kNqbc +U81YGhmfOrQ3iFNYBlSAseIHdAw39HGyC6OKzTXI4HRpc8CwcF6hKExkyWlkALr5 +i+IQDfimvarjjZ6Nm368L0Rthv3KOkI5CqRZ6bsVwwBug7GcdkvFs3LiRSKlMBpH +2tCkZ5ZZE8VyuK7VnlwV7n6EHzN5BqaHq8HVLw2KzvibSi+/5wIZV2Yx33tViLbh +OsZqLt6qQCGGgKzNX4TGwRLGAiVV1NCpgQhimZ4YP2thqSsqbaISOuvFlYq+QGP1 +bcvcHB7UhT1ZnHSDYcbT2qiD3VoqytXVKLB1X5XCD99YLSP9B32f1lvZD4MhDtE4 +IhAuqn15MGB5ct4yj/uMldFScs9KhqnWcwS4K6Qx3IfdB+ZxT5hEOWJLEcGqe/CS +XITNG7oS9mrSAJJvHSLz++4R/Sh1MnT2YWjyDk6qeeqAwut0w5iDKWt7qsGEcHFP +IVVlos+xLfrPDtgHQk8upjslUcMyMDTf21Y3RdJ3k1gTR9KHEwzKeiNlLjen9ekF +WupF8jik1aYRWL6h54ZyGxwKEyMYi9o18G2pXPzvVaPYtU+TGXdO4QwiES72TNCD +bNaGj75Gj0sN+LfjjQ4A898CAwEAAQ== +-----END PUBLIC KEY----- +""" +client_id = "manager-client" [account] admin_email = "admin@example.com" diff --git a/config/manager_config.go b/config/manager_config.go index 849c97a..729f9c6 100644 --- a/config/manager_config.go +++ b/config/manager_config.go @@ -55,6 +55,8 @@ func (mc MongoDBConfig) GetURI() string { type KeyConfig struct { RsaPrivateKeyPem SecretValue `mapstructure:"rsa_private_key_pem"` + DMPublicKeyPem SecretValue `mapstructure:"dm_public_key_pem"` + ClientID string `mapstructure:"client_id"` } type AccountConfig struct { diff --git a/config/manager_config.test.toml b/config/manager_config.test.toml index 4cb483d..037fa8b 100644 --- a/config/manager_config.test.toml +++ b/config/manager_config.test.toml @@ -69,6 +69,22 @@ spxkAVwlY6g1ZER7IFXlzhz6wYuDBayRhA/2zBPgtGesfpTd7H24AlJ6qB+mThHs X6m7Mp9nAMhRyXhULslO3trWFbFCa2dbQkDSyBRvsb2HZtztoLVyo1mtUg== -----END RSA PRIVATE KEY----- """ +dm_public_key_pem = """ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAny28YMC2/+yYj3T29lz6 +0uryNz8gNVrqD7lTJuHQ3DMTE6ADqnERy8VgHve0tWzhJc5ZBZ1Hduvj+z/kNqbc +U81YGhmfOrQ3iFNYBlSAseIHdAw39HGyC6OKzTXI4HRpc8CwcF6hKExkyWlkALr5 +i+IQDfimvarjjZ6Nm368L0Rthv3KOkI5CqRZ6bsVwwBug7GcdkvFs3LiRSKlMBpH +2tCkZ5ZZE8VyuK7VnlwV7n6EHzN5BqaHq8HVLw2KzvibSi+/5wIZV2Yx33tViLbh +OsZqLt6qQCGGgKzNX4TGwRLGAiVV1NCpgQhimZ4YP2thqSsqbaISOuvFlYq+QGP1 +bcvcHB7UhT1ZnHSDYcbT2qiD3VoqytXVKLB1X5XCD99YLSP9B32f1lvZD4MhDtE4 +IhAuqn15MGB5ct4yj/uMldFScs9KhqnWcwS4K6Qx3IfdB+ZxT5hEOWJLEcGqe/CS +XITNG7oS9mrSAJJvHSLz++4R/Sh1MnT2YWjyDk6qeeqAwut0w5iDKWt7qsGEcHFP +IVVlos+xLfrPDtgHQk8upjslUcMyMDTf21Y3RdJ3k1gTR9KHEwzKeiNlLjen9ekF +WupF8jik1aYRWL6h54ZyGxwKEyMYi9o18G2pXPzvVaPYtU+TGXdO4QwiES72TNCD +bNaGj75Gj0sN+LfjjQ4A898CAwEAAQ== +-----END PUBLIC KEY----- +""" [account] admin_email = "admin@example.com" diff --git a/decisionmaker/app/module.go b/decisionmaker/app/module.go index 3de4872..b6b40c2 100644 --- a/decisionmaker/app/module.go +++ b/decisionmaker/app/module.go @@ -16,14 +16,15 @@ func ConfigModule(cfg config.DecisionMakerConfig) (fx.Option, error) { fx.Provide(func(dmCfg config.DecisionMakerConfig) config.ServerConfig { return dmCfg.Server }), + fx.Provide(func(dmCfg config.DecisionMakerConfig) config.TokenConfig { + return dmCfg.Token + }), ), nil } func ServiceModule() (fx.Option, error) { return fx.Options( - fx.Provide(func() service.Service { - return service.NewService() - }), + fx.Provide(service.NewService), ), nil } diff --git a/decisionmaker/domain/metric_set.go b/decisionmaker/domain/metric_set.go new file mode 100644 index 0000000..9bb15f1 --- /dev/null +++ b/decisionmaker/domain/metric_set.go @@ -0,0 +1,15 @@ +package domain + +type MetricSet struct { + UserSchedLastRunAt uint64 + NrQueued uint64 + NrScheduled uint64 + NrRunning uint64 + NrOnlineCPUs uint64 + NrUserDispatches uint64 + NrKernelDispatches uint64 + NrCancelDispatches uint64 + NrBounceDispatches uint64 + NrFailedDispatches uint64 + NrSchedCongested uint64 +} diff --git a/decisionmaker/rest/handler.go b/decisionmaker/rest/handler.go index c2522af..8f0cead 100644 --- a/decisionmaker/rest/handler.go +++ b/decisionmaker/rest/handler.go @@ -6,11 +6,13 @@ import ( "net/http" "time" + "github.com/Gthulhu/api/config" "github.com/Gthulhu/api/decisionmaker/service" "github.com/Gthulhu/api/manager/errs" "github.com/Gthulhu/api/pkg/logger" "github.com/Gthulhu/api/pkg/middleware" "github.com/labstack/echo/v4" + "github.com/prometheus/client_golang/prometheus/promhttp" "go.uber.org/fx" ) @@ -54,17 +56,20 @@ type SuccessResponse[T any] struct { type Params struct { fx.In - Service service.Service + Service service.Service + TokenConfig config.TokenConfig } func NewHandler(params Params) (*Handler, error) { return &Handler{ - Service: params.Service, + Service: params.Service, + TokenConfig: params.TokenConfig, }, nil } type Handler struct { - Service service.Service + Service service.Service + TokenConfig config.TokenConfig } func (h *Handler) JSONResponse(ctx context.Context, w http.ResponseWriter, status int, data any) { @@ -142,7 +147,12 @@ func (h *Handler) HealthCheck(w http.ResponseWriter, r *http.Request) { h.JSONResponse(r.Context(), w, http.StatusOK, response) } -func (h *Handler) SetupRoutes(engine *echo.Echo) { +func (h *Handler) SetupRoutes(engine *echo.Echo) error { + authMiddleware, err := GetJwtAuthMiddleware(h.TokenConfig) + if err != nil { + return err + } + engine.GET("/health", h.echoHandler(h.HealthCheck)) engine.GET("/version", h.echoHandler(h.Version)) @@ -151,10 +161,18 @@ func (h *Handler) SetupRoutes(engine *echo.Echo) { { apiV1 := api.Group("/v1") // auth routes - apiV1.POST("/intents", h.echoHandler(h.HandleIntents)) - apiV1.GET("/scheduling/strategies", h.echoHandler(h.ListIntents)) + apiV1.POST("/intents", h.echoHandler(h.HandleIntents), echo.WrapMiddleware(authMiddleware)) + apiV1.GET("/scheduling/strategies", h.echoHandler(h.ListIntents), echo.WrapMiddleware(authMiddleware)) + apiV1.POST("/metrics", h.echoHandler(h.UpdateMetrics), echo.WrapMiddleware(authMiddleware)) + // token routes + apiV1.POST("/auth/token", h.echoHandler(h.GenTokenHandler)) } + // set up prometheus metrics endpoint + { + engine.GET("/metrics", echo.WrapHandler(promhttp.Handler())) + } + return nil } func (h *Handler) echoHandler(handlerFunc func(w http.ResponseWriter, r *http.Request)) echo.HandlerFunc { diff --git a/decisionmaker/rest/metric_handler.go b/decisionmaker/rest/metric_handler.go new file mode 100644 index 0000000..3405296 --- /dev/null +++ b/decisionmaker/rest/metric_handler.go @@ -0,0 +1,48 @@ +package rest + +import ( + "net/http" + + "github.com/Gthulhu/api/decisionmaker/domain" +) + +// UpdateMetricsRequest represents the payload for updating metrics. +type UpdateMetricsRequest struct { + Usersched_last_run_at uint64 `json:"usersched_last_run_at"` // The PID of the userspace scheduler + Nr_queued uint64 `json:"nr_queued"` // Number of tasks queued in the userspace scheduler + Nr_scheduled uint64 `json:"nr_scheduled"` // Number of tasks scheduled by the userspace scheduler + Nr_running uint64 `json:"nr_running"` // Number of tasks currently running in the userspace scheduler + Nr_online_cpus uint64 `json:"nr_online_cpus"` // Number of online CPUs in the system + Nr_user_dispatches uint64 `json:"nr_user_dispatches"` // Number of user-space dispatches + Nr_kernel_dispatches uint64 `json:"nr_kernel_dispatches"` // Number of kernel-space dispatches + Nr_cancel_dispatches uint64 `json:"nr_cancel_dispatches"` // Number of cancelled dispatches + Nr_bounce_dispatches uint64 `json:"nr_bounce_dispatches"` // Number of bounce dispatches + Nr_failed_dispatches uint64 `json:"nr_failed_dispatches"` // Number of failed dispatches + Nr_sched_congested uint64 `json:"nr_sched_congested"` // Number of times the scheduler was congested +} + +// UpdateMetrics handles the updating of metrics via a REST endpoint. +func (h *Handler) UpdateMetrics(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var req UpdateMetricsRequest + err := h.JSONBind(r, &req) + if err != nil { + h.ErrorResponse(ctx, w, http.StatusBadRequest, "Invalid request payload", err) + return + } + newMetricSet := &domain.MetricSet{ + UserSchedLastRunAt: req.Usersched_last_run_at, + NrQueued: req.Nr_queued, + NrScheduled: req.Nr_scheduled, + NrRunning: req.Nr_running, + NrOnlineCPUs: req.Nr_online_cpus, + NrUserDispatches: req.Nr_user_dispatches, + NrKernelDispatches: req.Nr_kernel_dispatches, + NrCancelDispatches: req.Nr_cancel_dispatches, + NrBounceDispatches: req.Nr_bounce_dispatches, + NrFailedDispatches: req.Nr_failed_dispatches, + NrSchedCongested: req.Nr_sched_congested, + } + h.Service.UpdateMetrics(r.Context(), newMetricSet) + h.JSONResponse(ctx, w, http.StatusOK, NewSuccessResponse[EmptyResponse](nil)) +} diff --git a/decisionmaker/rest/middleware.go b/decisionmaker/rest/middleware.go new file mode 100644 index 0000000..a1fe265 --- /dev/null +++ b/decisionmaker/rest/middleware.go @@ -0,0 +1,107 @@ +package rest + +import ( + "crypto/rsa" + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/Gthulhu/api/config" + "github.com/Gthulhu/api/pkg/logger" + "github.com/Gthulhu/api/pkg/util" + "github.com/golang-jwt/jwt/v5" +) + +func GetJwtAuthMiddleware(tokenConfig config.TokenConfig) (func(next http.Handler) http.Handler, error) { + rasKey, err := util.InitRSAPrivateKey(string(tokenConfig.RsaPrivateKeyPem)) + if err != nil { + return nil, err + } + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Skip auth for OPTIONS requests, health check, root endpoint, token endpoint, and static files + if r.Method == "OPTIONS" || + r.URL.Path == "/health" || + r.URL.Path == "/" || + r.URL.Path == "/api/v1/auth/token" || + strings.HasPrefix(r.URL.Path, "/static/") { + next.ServeHTTP(w, r) + return + } + + // Extract token from Authorization header + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + if err := json.NewEncoder(w).Encode(ErrorResponse{ + Success: false, + Error: "Authorization header is required", + }); err != nil { + logger.Logger(r.Context()).Error().Err(err).Msg("Failed to write unauthorized response") + } + return + } + + // Check Bearer token format + const bearerSchema = "Bearer " + if !strings.HasPrefix(authHeader, bearerSchema) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + if err := json.NewEncoder(w).Encode(ErrorResponse{ + Success: false, + Error: "Authorization header must start with 'Bearer '", + }); err != nil { + logger.Logger(r.Context()).Error().Err(err).Msg("Failed to write unauthorized response") + } + return + } + + tokenString := authHeader[len(bearerSchema):] + + // Validate JWT token + claims, err := validateJWT(rasKey, tokenString) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + if err := json.NewEncoder(w).Encode(ErrorResponse{ + Success: false, + Error: "Invalid or expired token: " + err.Error(), + }); err != nil { + logger.Logger(r.Context()).Error().Err(err).Msg("Failed to write unauthorized response") + } + return + } + + logger.Logger(r.Context()).Info().Str("client_id", claims.ClientID).Msg("JWT token validated successfully") + next.ServeHTTP(w, r) + }) + }, nil +} + +// Claims represents JWT token claims +type Claims struct { + ClientID string `json:"client_id"` + jwt.RegisteredClaims +} + +// validateJWT validates a JWT token and returns the claims +func validateJWT(rasKey *rsa.PrivateKey, tokenString string) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return &rasKey.PublicKey, nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + + return nil, fmt.Errorf("invalid token") +} diff --git a/decisionmaker/rest/token_handler.go b/decisionmaker/rest/token_handler.go new file mode 100644 index 0000000..ab2e09d --- /dev/null +++ b/decisionmaker/rest/token_handler.go @@ -0,0 +1,40 @@ +package rest + +import ( + "net/http" +) + +// TokenResponse represents the response structure for JWT token generation +type TokenResponse struct { + Token string `json:"token,omitempty"` + ExpiredAt int64 `json:"expired_at,omitempty"` +} + +// TokenRequest represents the request structure for JWT token generation +type TokenRequest struct { + PublicKey string `json:"public_key"` // PEM encoded public key + ClientID string `json:"client_id"` // Client identifier + ExpiredAt int64 `json:"expired_at"` // Expiration timestamp +} + +// GenTokenHandler handles JWT token generation upon public key verification +func (h Handler) GenTokenHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var req TokenRequest + err := h.JSONBind(r, &req) + if err != nil { + h.ErrorResponse(ctx, w, http.StatusBadRequest, "Invalid request payload", err) + return + } + token, expiredAt, err := h.Service.VerifyAndGenerateToken(r.Context(), req.ClientID, req.PublicKey) + if err != nil { + h.ErrorResponse(ctx, w, http.StatusUnauthorized, "Public key verification failed ", err) + return + } + + resp := TokenResponse{ + ExpiredAt: expiredAt, + Token: token, + } + h.JSONResponse(ctx, w, http.StatusOK, NewSuccessResponse[TokenResponse](&resp)) +} diff --git a/decisionmaker/service/metric_collector.go b/decisionmaker/service/metric_collector.go new file mode 100644 index 0000000..7fbff86 --- /dev/null +++ b/decisionmaker/service/metric_collector.go @@ -0,0 +1,144 @@ +package service + +import ( + "sync/atomic" + + "github.com/Gthulhu/api/decisionmaker/domain" + "github.com/prometheus/client_golang/prometheus" +) + +var ( + _ prometheus.Collector = (*MetricCollector)(nil) +) + +// MetricCollector +type MetricCollector struct { + machineID string + + UserSchedLastRunAtMetric *prometheus.Desc + NrQueuedMetric *prometheus.Desc + NrScheduledMetric *prometheus.Desc + NrRunningMetric *prometheus.Desc + NrOnlineCPUsMetric *prometheus.Desc + NrUserDispatchesMetric *prometheus.Desc + NrKernelDispatchesMetric *prometheus.Desc + NrCancelDispatchesMetric *prometheus.Desc + NrBounceDispatchesMetric *prometheus.Desc + NrFailedDispatchesMetric *prometheus.Desc + NrSchedCongestedMetric *prometheus.Desc + + metricSet atomic.Pointer[domain.MetricSet] +} + +// // NewMetricCollector creates a new MetricCollector for a specific game server. +func NewMetricCollector(machineID string) *MetricCollector { + constantLabels := prometheus.Labels{"machine_id": machineID} + return &MetricCollector{ + machineID: machineID, + UserSchedLastRunAtMetric: prometheus.NewDesc( + "user_sched_last_run_at", + "the timestamp of the last user scheduling run", + nil, + constantLabels, + ), + NrQueuedMetric: prometheus.NewDesc( + "nr_queued", + "number of tasks queued in the userspace scheduler", + nil, + constantLabels, + ), + NrScheduledMetric: prometheus.NewDesc( + "nr_scheduled", + "number of tasks scheduled by the userspace scheduler", + nil, + constantLabels, + ), + NrRunningMetric: prometheus.NewDesc( + "nr_running", + "number of tasks currently running in the userspace scheduler", + nil, + constantLabels, + ), + NrOnlineCPUsMetric: prometheus.NewDesc( + "nr_online_cpus", + "number of online CPUs in the system", + nil, + constantLabels, + ), + NrUserDispatchesMetric: prometheus.NewDesc( + "nr_user_dispatches", + "number of user-space dispatches", + nil, + constantLabels, + ), + NrKernelDispatchesMetric: prometheus.NewDesc( + "nr_kernel_dispatches", + "number of kernel-space dispatches", + nil, + constantLabels, + ), + NrCancelDispatchesMetric: prometheus.NewDesc( + "nr_cancel_dispatches", + "number of canceled dispatches", + nil, + constantLabels, + ), + NrBounceDispatchesMetric: prometheus.NewDesc( + "nr_bounce_dispatches", + "number of bounced dispatches", + nil, + constantLabels, + ), + NrFailedDispatchesMetric: prometheus.NewDesc( + "nr_failed_dispatches", + "number of failed dispatches", + nil, + constantLabels, + ), + NrSchedCongestedMetric: prometheus.NewDesc( + "nr_sched_congested", + "number of times the scheduler was congested", + nil, + constantLabels, + ), + } +} + +// Describe sends the super-set of all possible descriptors of metrics +func (collector *MetricCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- collector.UserSchedLastRunAtMetric + ch <- collector.NrQueuedMetric + ch <- collector.NrScheduledMetric + ch <- collector.NrRunningMetric + ch <- collector.NrOnlineCPUsMetric + ch <- collector.NrUserDispatchesMetric + ch <- collector.NrKernelDispatchesMetric + ch <- collector.NrCancelDispatchesMetric + ch <- collector.NrBounceDispatchesMetric + ch <- collector.NrFailedDispatchesMetric + ch <- collector.NrSchedCongestedMetric +} + +// Collect is called by the Prometheus registry when collecting metrics. +func (collector *MetricCollector) Collect(ch chan<- prometheus.Metric) { + metricSet := collector.metricSet.Load() + if metricSet == nil { + return + } + + ch <- prometheus.MustNewConstMetric(collector.UserSchedLastRunAtMetric, prometheus.GaugeValue, float64(metricSet.UserSchedLastRunAt)) + ch <- prometheus.MustNewConstMetric(collector.NrQueuedMetric, prometheus.GaugeValue, float64(metricSet.NrQueued)) + ch <- prometheus.MustNewConstMetric(collector.NrScheduledMetric, prometheus.GaugeValue, float64(metricSet.NrScheduled)) + ch <- prometheus.MustNewConstMetric(collector.NrRunningMetric, prometheus.GaugeValue, float64(metricSet.NrRunning)) + ch <- prometheus.MustNewConstMetric(collector.NrOnlineCPUsMetric, prometheus.GaugeValue, float64(metricSet.NrOnlineCPUs)) + ch <- prometheus.MustNewConstMetric(collector.NrUserDispatchesMetric, prometheus.GaugeValue, float64(metricSet.NrUserDispatches)) + ch <- prometheus.MustNewConstMetric(collector.NrKernelDispatchesMetric, prometheus.GaugeValue, float64(metricSet.NrKernelDispatches)) + ch <- prometheus.MustNewConstMetric(collector.NrCancelDispatchesMetric, prometheus.GaugeValue, float64(metricSet.NrCancelDispatches)) + ch <- prometheus.MustNewConstMetric(collector.NrBounceDispatchesMetric, prometheus.GaugeValue, float64(metricSet.NrBounceDispatches)) + ch <- prometheus.MustNewConstMetric(collector.NrFailedDispatchesMetric, prometheus.GaugeValue, float64(metricSet.NrFailedDispatches)) + ch <- prometheus.MustNewConstMetric(collector.NrSchedCongestedMetric, prometheus.GaugeValue, float64(metricSet.NrSchedCongested)) +} + +func (collector *MetricCollector) UpdateMetrics(newMetricSet *domain.MetricSet) { + collector.metricSet.Store(newMetricSet) +} diff --git a/decisionmaker/service/service.go b/decisionmaker/service/service.go index 4c17401..ef3e1f0 100644 --- a/decisionmaker/service/service.go +++ b/decisionmaker/service/service.go @@ -3,25 +3,49 @@ package service import ( "bufio" "context" + "crypto/rsa" "fmt" "os" "regexp" "strconv" "strings" + "github.com/Gthulhu/api/config" "github.com/Gthulhu/api/decisionmaker/domain" "github.com/Gthulhu/api/pkg/logger" "github.com/Gthulhu/api/pkg/util" + "github.com/prometheus/client_golang/prometheus" + "go.uber.org/fx" ) -func NewService() Service { - return Service{ +type Params struct { + fx.In + TokenConfig config.TokenConfig +} + +func NewService(params Params) (Service, error) { + privateKey, err := util.InitRSAPrivateKey(string(params.TokenConfig.RsaPrivateKeyPem)) + if err != nil { + return Service{}, fmt.Errorf("failed to initialize JWT private key: %v", err) + } + svc := Service{ schedulingIntentsMap: util.NewGenericMap[string, []*domain.SchedulingIntents](), + metricCollector: NewMetricCollector(util.GetMachineID()), + jwtPrivateKey: privateKey, } + + err = prometheus.Register(svc.metricCollector) + if err != nil { + return Service{}, fmt.Errorf("failed to register metric collector: %v", err) + } + return svc, nil } type Service struct { schedulingIntentsMap *util.GenericMap[string, []*domain.SchedulingIntents] + metricCollector *MetricCollector + jwtPrivateKey *rsa.PrivateKey + tokenConfig config.TokenConfig } const ( @@ -212,3 +236,7 @@ func (svc *Service) getProcessInfo(rootDir string, pid int) (domain.PodProcess, return process, nil } + +func (svc *Service) UpdateMetrics(ctx context.Context, newMetricSet *domain.MetricSet) { + svc.metricCollector.UpdateMetrics(newMetricSet) +} diff --git a/decisionmaker/service/token_svc.go b/decisionmaker/service/token_svc.go new file mode 100644 index 0000000..7c2277a --- /dev/null +++ b/decisionmaker/service/token_svc.go @@ -0,0 +1,71 @@ +package service + +import ( + "context" + "fmt" + "time" + + "github.com/Gthulhu/api/pkg/logger" + "github.com/Gthulhu/api/pkg/util" + "github.com/golang-jwt/jwt/v5" +) + +// VerifyAndGenerateToken verifies the provided public key and generates a JWT token if valid +func (svc *Service) VerifyAndGenerateToken(ctx context.Context, clientID string, publicKey string) (string, int64, error) { + err := svc.VerifyPublicKey(publicKey) + if err != nil { + return "", 0, fmt.Errorf("public key verification failed: %v", err) + } + token, claims, err := svc.generateJWT(ctx, clientID) + if err != nil { + return "", 0, fmt.Errorf("JWT generation failed: %v", err) + } + return token, claims.ExpiresAt.Unix(), nil +} + +// verifyPublicKey verifies if the provided public key matches our private key +func (svc *Service) VerifyPublicKey(publicKeyPEM string) error { + rsaPublicKey, err := util.PEMToRSAPublicKey(publicKeyPEM) + if err != nil { + return fmt.Errorf("failed to parse public key: %v", err) + } + // Compare public key with our private key's public key + if !rsaPublicKey.Equal(&svc.jwtPrivateKey.PublicKey) { + return fmt.Errorf("public key does not match server's private key") + } + + return nil +} + +// generateJWT generates a JWT token for authenticated client +func (svc *Service) generateJWT(ctx context.Context, clientID string) (string, Claims, error) { + expireHr := svc.tokenConfig.TokenDurationHr + if expireHr <= 0 { + logger.Logger(ctx).Warn().Msgf("invalid token duration hr %d, defaulting to 24 hours", expireHr) + expireHr = 24 // default to 24 hours + } + + claims := Claims{ + ClientID: clientID, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expireHr) * time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Issuer: "decision-maker-service", + Subject: clientID, + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + tokenStr, err := token.SignedString(svc.jwtPrivateKey) + if err != nil { + return "", Claims{}, fmt.Errorf("failed to sign JWT token: %v", err) + } + return tokenStr, claims, nil +} + +// Claims represents JWT token claims +type Claims struct { + ClientID string `json:"client_id"` + jwt.RegisteredClaims +} diff --git a/deployment/kind/local_test.sh b/deployment/kind/local_test.sh index 5dedfea..8ee9ab3 100644 --- a/deployment/kind/local_test.sh +++ b/deployment/kind/local_test.sh @@ -1,8 +1,45 @@ -curl -X 'POST' \ -'http://localhost:8080/api/v1/auth/login' \ --H 'accept: application/json' \ --H 'Content-Type: application/json' \ --d '{ -"password": "your-password-here", -"username": "admin@example.com" -}' \ No newline at end of file +#!/usr/bin/env bash +set -euo pipefail + +BASE_URL="http://localhost:8080" +USERNAME="admin@example.com" +PASSWORD="your-password-here" + +echo "[1] Login..." + +LOGIN_RESP=$(curl -s -X POST \ + "$BASE_URL/api/v1/auth/login" \ + -H "Content-Type: application/json" \ + -H "accept: application/json" \ + -d "{ + \"username\": \"$USERNAME\", + \"password\": \"$PASSWORD\" + }") + +TOKEN=$(echo "$LOGIN_RESP" | jq -r '.data.token') + +if [[ -z "$TOKEN" || "$TOKEN" == "null" ]]; then + echo "❌ Failed to get token" + echo "$LOGIN_RESP" + exit 1 +fi + +echo "✅ Token acquired" + +echo "[2] Create strategy..." + +curl -s -X POST \ + "$BASE_URL/api/v1/strategies" \ + -H "Content-Type: application/json" \ + -H "accept: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{ + "labelSelectors": [ + { + "key": "app", + "value": "busybox" + } + ], + "priority": 1 + }' | jq +echo "✅ Strategy created" \ No newline at end of file diff --git a/deployment/kind/mongo/statefulset.yaml b/deployment/kind/mongo/statefulset.yaml index 5259ad9..b5ef948 100644 --- a/deployment/kind/mongo/statefulset.yaml +++ b/deployment/kind/mongo/statefulset.yaml @@ -97,6 +97,13 @@ spec: }); print("Replica set initiated"); } + while (true) { + const hello = db.hello(); + if (hello.isWritablePrimary) { + break; + } + sleep(1000); + } db = db.getSiblingDB("admin"); try { db.getUser("'"$MONGO_ROOT_USERNAME"'"); diff --git a/go.mod b/go.mod index c13ebcc..211e2ef 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,13 @@ toolchain go1.24.5 tool github.com/vektra/mockery/v3 require ( + github.com/Code-Hex/go-generics-cache v1.5.1 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/golang-migrate/migrate/v4 v4.19.1 github.com/labstack/echo/v4 v4.13.4 github.com/ory/dockertest/v3 v3.12.0 github.com/pkg/errors v0.9.1 + github.com/prometheus/client_golang v1.23.2 github.com/rs/xid v1.5.0 github.com/rs/zerolog v1.33.0 github.com/spf13/cobra v1.8.1 @@ -34,8 +36,10 @@ require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/brunoga/deep v1.2.4 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/continuity v0.4.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/docker/cli v27.4.1+incompatible // indirect @@ -67,7 +71,7 @@ require ( github.com/jedib0t/go-pretty/v6 v6.6.7 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.16.7 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect github.com/knadh/koanf/parsers/yaml v0.1.0 // indirect github.com/knadh/koanf/providers/env v1.0.0 // indirect @@ -94,6 +98,9 @@ require ( github.com/opencontainers/runc v1.2.3 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect @@ -119,6 +126,7 @@ require ( go.uber.org/dig v1.19.0 // indirect go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.26.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.29.0 // indirect @@ -130,7 +138,7 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.38.0 // indirect - google.golang.org/protobuf v1.36.7 // indirect + google.golang.org/protobuf v1.36.8 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index ec23dd5..2fed92d 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Code-Hex/go-generics-cache v1.5.1 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U+0KMqAA0KcU= +github.com/Code-Hex/go-generics-cache v1.5.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -12,10 +14,14 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEV github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +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/brunoga/deep v1.2.4 h1:Aj9E9oUbE+ccbyh35VC/NHlzzjfIVU69BXu2mt2LmL8= github.com/brunoga/deep v1.2.4/go.mod h1:GDV6dnXqn80ezsLSZ5Wlv1PdKAWAO4L5PnKYtv2dgaI= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -120,8 +126,8 @@ 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/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= @@ -144,6 +150,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= @@ -206,6 +214,14 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE 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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -304,6 +320,8 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -375,8 +393,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= -google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/manager/app/module.go b/manager/app/module.go index 5aac5eb..508854d 100644 --- a/manager/app/module.go +++ b/manager/app/module.go @@ -45,9 +45,7 @@ func AdapterModule() (fx.Option, error) { InCluster: k8sConfig.IsInCluster, }) }), - fx.Provide(func() domain.DecisionMakerAdapter { - return client.NewDecisionMakerClient() - }), + fx.Provide(client.NewDecisionMakerClient), ), nil } diff --git a/manager/client/deicison_maker.go b/manager/client/deicison_maker.go index 6b4e749..d2df6df 100644 --- a/manager/client/deicison_maker.go +++ b/manager/client/deicison_maker.go @@ -7,23 +7,38 @@ import ( "fmt" "net/http" "strconv" + "time" + cache "github.com/Code-Hex/go-generics-cache" + "github.com/Gthulhu/api/config" dmrest "github.com/Gthulhu/api/decisionmaker/rest" "github.com/Gthulhu/api/manager/domain" "github.com/Gthulhu/api/pkg/logger" ) -func NewDecisionMakerClient() domain.DecisionMakerAdapter { +func NewDecisionMakerClient(keyConfig config.KeyConfig) domain.DecisionMakerAdapter { return &DecisionMakerClient{ - Client: http.DefaultClient, + Client: http.DefaultClient, + tokenPublicKey: keyConfig.DMPublicKeyPem.Value(), + clientID: keyConfig.ClientID, + tokenCache: cache.New[string, string](), } } type DecisionMakerClient struct { *http.Client + + tokenPublicKey string + clientID string + tokenCache *cache.Cache[string, string] } -func (dm DecisionMakerClient) SendSchedulingIntent(ctx context.Context, decisionMaker *domain.DecisionMakerPod, intents []*domain.ScheduleIntent) error { +func (dm *DecisionMakerClient) SendSchedulingIntent(ctx context.Context, decisionMaker *domain.DecisionMakerPod, intents []*domain.ScheduleIntent) error { + token, err := dm.GetToken(ctx, decisionMaker) + if err != nil { + return err + } + logger.Logger(ctx).Debug().Msgf("Sending %d scheduling intents to decision maker pod (host:%s nodeID:%s port:%d)", len(intents), decisionMaker.Host, decisionMaker.NodeID, decisionMaker.Port) reqPayload := dmrest.HandleIntentsRequest{ @@ -52,8 +67,7 @@ func (dm DecisionMakerClient) SendSchedulingIntent(ctx context.Context, decision return err } req.Header.Set("Content-Type", "application/json") - // TODO: add authentication headers if needed - + req.Header.Set("Authorization", "Bearer "+token) resp, err := dm.Client.Do(req) if err != nil { return err @@ -64,3 +78,44 @@ func (dm DecisionMakerClient) SendSchedulingIntent(ctx context.Context, decision } return nil } + +func (dm *DecisionMakerClient) GetToken(ctx context.Context, decisionMaker *domain.DecisionMakerPod) (string, error) { + if token, ok := dm.tokenCache.Get(decisionMaker.NodeID); ok { + return token, nil + } + + req := dmrest.TokenRequest{ + PublicKey: dm.tokenPublicKey, + ClientID: dm.clientID, + } + jsonBody, err := json.Marshal(req) + if err != nil { + return "", err + } + endpoint := "http://" + decisionMaker.Host + ":" + strconv.Itoa(decisionMaker.Port) + "/api/v1/auth/token" + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(jsonBody)) + if err != nil { + return "", err + } + httpReq.Header.Set("Content-Type", "application/json") + resp, err := dm.Client.Do(httpReq) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("decision maker %s returned non-OK status: %s", decisionMaker, resp.Status) + } + var tokenResp dmrest.SuccessResponse[dmrest.TokenResponse] + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&tokenResp) + if err != nil { + return "", err + } + + ttl := time.Now().Unix() - tokenResp.Data.ExpiredAt - 60 + + dm.tokenCache.Set(decisionMaker.NodeID, tokenResp.Data.Token, cache.WithExpiration(time.Duration(ttl)*time.Second)) + return tokenResp.Data.Token, nil + +} diff --git a/manager/domain/mock_domain.go b/manager/domain/mock_domain.go index 2960046..a5063c4 100644 --- a/manager/domain/mock_domain.go +++ b/manager/domain/mock_domain.go @@ -1322,6 +1322,120 @@ func (_c *MockService_DeleteRole_Call) RunAndReturn(run func(ctx context.Context return _c } +// ListScheduleIntents provides a mock function for the type MockService +func (_mock *MockService) ListScheduleIntents(ctx context.Context, filterOpts *QueryIntentOptions) error { + ret := _mock.Called(ctx, filterOpts) + + if len(ret) == 0 { + panic("no return value specified for ListScheduleIntents") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *QueryIntentOptions) error); ok { + r0 = returnFunc(ctx, filterOpts) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockService_ListScheduleIntents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListScheduleIntents' +type MockService_ListScheduleIntents_Call struct { + *mock.Call +} + +// ListScheduleIntents is a helper method to define mock.On call +// - ctx context.Context +// - filterOpts *QueryIntentOptions +func (_e *MockService_Expecter) ListScheduleIntents(ctx interface{}, filterOpts interface{}) *MockService_ListScheduleIntents_Call { + return &MockService_ListScheduleIntents_Call{Call: _e.mock.On("ListScheduleIntents", ctx, filterOpts)} +} + +func (_c *MockService_ListScheduleIntents_Call) Run(run func(ctx context.Context, filterOpts *QueryIntentOptions)) *MockService_ListScheduleIntents_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *QueryIntentOptions + if args[1] != nil { + arg1 = args[1].(*QueryIntentOptions) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockService_ListScheduleIntents_Call) Return(err error) *MockService_ListScheduleIntents_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockService_ListScheduleIntents_Call) RunAndReturn(run func(ctx context.Context, filterOpts *QueryIntentOptions) error) *MockService_ListScheduleIntents_Call { + _c.Call.Return(run) + return _c +} + +// ListScheduleStrategies provides a mock function for the type MockService +func (_mock *MockService) ListScheduleStrategies(ctx context.Context, filterOpts *QueryStrategyOptions) error { + ret := _mock.Called(ctx, filterOpts) + + if len(ret) == 0 { + panic("no return value specified for ListScheduleStrategies") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *QueryStrategyOptions) error); ok { + r0 = returnFunc(ctx, filterOpts) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockService_ListScheduleStrategies_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListScheduleStrategies' +type MockService_ListScheduleStrategies_Call struct { + *mock.Call +} + +// ListScheduleStrategies is a helper method to define mock.On call +// - ctx context.Context +// - filterOpts *QueryStrategyOptions +func (_e *MockService_Expecter) ListScheduleStrategies(ctx interface{}, filterOpts interface{}) *MockService_ListScheduleStrategies_Call { + return &MockService_ListScheduleStrategies_Call{Call: _e.mock.On("ListScheduleStrategies", ctx, filterOpts)} +} + +func (_c *MockService_ListScheduleStrategies_Call) Run(run func(ctx context.Context, filterOpts *QueryStrategyOptions)) *MockService_ListScheduleStrategies_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *QueryStrategyOptions + if args[1] != nil { + arg1 = args[1].(*QueryStrategyOptions) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockService_ListScheduleStrategies_Call) Return(err error) *MockService_ListScheduleStrategies_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockService_ListScheduleStrategies_Call) RunAndReturn(run func(ctx context.Context, filterOpts *QueryStrategyOptions) error) *MockService_ListScheduleStrategies_Call { + _c.Call.Return(run) + return _c +} + // Login provides a mock function for the type MockService func (_mock *MockService) Login(ctx context.Context, email string, password string) (string, error) { ret := _mock.Called(ctx, email, password) diff --git a/pkg/util/encrypt.go b/pkg/util/encrypt.go index d86360b..66612c8 100644 --- a/pkg/util/encrypt.go +++ b/pkg/util/encrypt.go @@ -2,8 +2,12 @@ package util import ( "crypto/rand" + "crypto/rsa" "crypto/subtle" + "crypto/x509" "encoding/base64" + "encoding/pem" + "errors" "fmt" "regexp" "strings" @@ -113,3 +117,58 @@ func decodeHash(encodedHash string) (p *Argon2idParams, salt, hash []byte, err e return p, salt, hash, nil } + +func InitRSAPrivateKey(pemStr string) (*rsa.PrivateKey, error) { + block, _ := pem.Decode([]byte(pemStr)) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM block containing private key") + } + + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + // Try PKCS8 format + keyInterface, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse private key: %v", err) + } + var ok bool + key, ok = keyInterface.(*rsa.PrivateKey) + if !ok { + return nil, fmt.Errorf("private key is not RSA") + } + } + return key, nil +} + +func RSAPublicKeyToPEM(pub *rsa.PublicKey) ([]byte, error) { + derBytes, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return nil, err + } + + block := &pem.Block{ + Type: "PUBLIC KEY", + Bytes: derBytes, + } + + return pem.EncodeToMemory(block), nil +} + +func PEMToRSAPublicKey(pemBytes string) (*rsa.PublicKey, error) { + block, _ := pem.Decode([]byte(pemBytes)) + if block == nil { + return nil, errors.New("failed to decode PEM block") + } + + pubAny, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, err + } + + pub, ok := pubAny.(*rsa.PublicKey) + if !ok { + return nil, errors.New("not an RSA public key") + } + + return pub, nil +} diff --git a/pkg/util/encrypt_test.go b/pkg/util/encrypt_test.go index 9437be4..d8f63f0 100644 --- a/pkg/util/encrypt_test.go +++ b/pkg/util/encrypt_test.go @@ -24,3 +24,72 @@ func TestAngron(t *testing.T) { require.NoError(t, err) assert.False(t, ok, "Wrong password should not match the hash") } + +func TestInitRsaKey(t *testing.T) { + rsaPem := ` +-----BEGIN RSA PRIVATE KEY----- +MIIJJwIBAAKCAgEAny28YMC2/+yYj3T29lz60uryNz8gNVrqD7lTJuHQ3DMTE6AD +qnERy8VgHve0tWzhJc5ZBZ1Hduvj+z/kNqbcU81YGhmfOrQ3iFNYBlSAseIHdAw3 +9HGyC6OKzTXI4HRpc8CwcF6hKExkyWlkALr5i+IQDfimvarjjZ6Nm368L0Rthv3K +OkI5CqRZ6bsVwwBug7GcdkvFs3LiRSKlMBpH2tCkZ5ZZE8VyuK7VnlwV7n6EHzN5 +BqaHq8HVLw2KzvibSi+/5wIZV2Yx33tViLbhOsZqLt6qQCGGgKzNX4TGwRLGAiVV +1NCpgQhimZ4YP2thqSsqbaISOuvFlYq+QGP1bcvcHB7UhT1ZnHSDYcbT2qiD3Voq +ytXVKLB1X5XCD99YLSP9B32f1lvZD4MhDtE4IhAuqn15MGB5ct4yj/uMldFScs9K +hqnWcwS4K6Qx3IfdB+ZxT5hEOWJLEcGqe/CSXITNG7oS9mrSAJJvHSLz++4R/Sh1 +MnT2YWjyDk6qeeqAwut0w5iDKWt7qsGEcHFPIVVlos+xLfrPDtgHQk8upjslUcMy +MDTf21Y3RdJ3k1gTR9KHEwzKeiNlLjen9ekFWupF8jik1aYRWL6h54ZyGxwKEyMY +i9o18G2pXPzvVaPYtU+TGXdO4QwiES72TNCDbNaGj75Gj0sN+LfjjQ4A898CAwEA +AQKCAgAFrHuqdzQOq0BE3MZwwZ+vJPC9R2K+hB8TsGdmW2Y2cxua93kp+h3IRaDH +eczXKqpbzp8dtB13/7CApCZeTFROKGObio5CaWoRUecxUpHDxWq+mDDmZacTAyFP +bztZxMx9c8DWQIk+BnsRMtB9tixu7//if5px6EV0JtKlWD8c8DN3PFSY/wNJfdI2 +opSD/t/xkcMh9FF3tACctj9tF4K4KfeyOYmzSrZsHs8+dcnSVnAfLJaDxivP03jl +1HW+Kt5eJpWQhmKg2uOsM5k45kvg7HGcehNXddp1e7NWVEVBXInySaJlk4p3LvVU +xG3Y1NsGTKOWhNBhiUXhrrBZWzbERLvE7/OtrHVDAlEuA47rFb/rTnMWHIZhNeXt +Hwa7G/11dlwYN3jn5u+/2SkVC0R4X/lqTzFRzCYIWr9lTeVpC53Gn1jmX2PSNDbF +yLi7ZZFBhS8GdNimeKyJReKV2o2nsO49KngGIs5/B318GA5BwBVPf9fVd9a2n29s +ioUXmTE07bpbyXk1BL4jZFOAJYqXh6IOwAFKFtQXgfidod7KcwpxQAJpE1Od3EPd +sGlTTC+hsUzAdlV82wqc6AB80DcxlzsI0adTkD7NrIcRSQtCC9DnZPXm/kFiam80 +gW2SmIsaLYauoQgIcbI1Lpy3rMCMbbTeG7K3KGyYZULFXHRsAQKCAQEAy5n5Xt8z +VaCS+V0VrwpIdPAOzgaxJkcfy3hVLe5vkhaeu5DzyHgW0fPud0P2J1eUD8GWxXvj +6EImFsFydH+DR8ClhHX8awpEn4mS6vR6VVPseqKWzs6BP7usH/WNTIeGJR2z0hRG +ZgD/z4W1PwwL6tIno/oHY3MkflX5/1X6q7vGjNjN7d81Mb48dXcLEjaZWtcRqzy0 +MNXrgvrpe/pCRUeUBV6WYHSPn6OPJA7RgcKegn9AwWEoecieFnjiVSjdfI2WT+Wa +13MCwPoWs1u+mHNl+yaqFnj9u9tF2xF6M8ZERscmvU+k0aDNcuJq/drP6qH/93gn +e7Tjwlf3IqhcLwKCAQEAyCUFaRimDfn43qLBDZpgmBHwCpBQFaNS8jvVQpUlQ4zL +W/pCqMIePajE5E9EBcQZyd+tE6WE202Z2CBzvfyH54HGmAiEE5koQSE6GBZbXrzu +4ToolPR6nrlEDR6ayjuv9BOPR91OZL9EfSgi5kdNIEilnDm1n/VhEIW5y6TsJUGv +VeuTDgnRYbkIVBBppA5U4rYyOz7ES+0L344k05i9LzFvcgc1QX50Dg8dqiI2tm6z +uyjxhJ9TW6R1iqzLnDB01YrDuWN62qISHnNz4X5Z/EBoITo3Og2IMaCeE5yKHCjw +FrrV05F8Cf7B5DLBOTWiX3oPtu9oV7QrjN6WARGHUQKCAQARrH8KLkPthe/cN6lf +NXxOslwGpGwST5BCAGMchpsmylHjJFUVLN+GQC+OKNcgWSjgKUTmRbfl/IAD76z4 +0ezaeK2ljvxnak/ErZOUU76e05cumhiPQTvVBXyOlak7YHRTmn12mg32YtXR9OBj +5a7PJokMYfLsPh2H3fzCnnsRF07IATX3FS4v8DydUcUjQpwTV6IQBEf8CUXVa+SC +v5mrG+iMgsZ4/wVMrU0Kq0KiiftqhpNfdgimcbTPbJTxIYgAfOX0b5D+bNxrVgpM +bYVhBHtwzs1q//u+p+0rdBvwjKB2qGkDe/tpuxS6iU8SVEFCM+fdWo/K3Ev9Hde1 +KXo/AoIBABUAdYHiuUIMMgZCs9lWkr5CW5rwK8cpfUG375fuCJv/ATPknewRepTj +yc1fV/b27fHWC9Zc7wUILpWUSjDsd+JeJtW7Rwi7cJLtBqiSaAIX90UhEjMXOGrB +bBeoV3vTKZKGHunemiROQcSUWp0pbDlwBhjPoXRojkfqkGWDJ9h8/QYaEzNM6nDD +ttEDa+JwMo4bqke3PWfuNum9g7XEeE2kdVpU0UzPFSSIh4db0bvw/+Eq2bUd9uRN +7JuhqDf6ibgCuKkSfEjG6vnRCZ7m4FBs/cBG2Ja55sm2XgAW1BNCZHcuIdPylz6B +Qh1NCiOTsjcsmsuKcbuKR2ufy8PO8BECggEACr4AaasrHcqqBcGCZfGlO4Q0Bbzu +1IJ6u9X+0U+k36wH9cE8RMJZaFo30bYTSBnxN6YY4M1taG3egIZQNgTRpjEiC8lV +spxkAVwlY6g1ZER7IFXlzhz6wYuDBayRhA/2zBPgtGesfpTd7H24AlJ6qB+mThHs +7ZbETM4KP04uNBBPybwaPIOkUPnQInfIPOAJVHebHp8atyavwrpGmRq958XKMgvz +8oHIdzV+XTMi1+U7eg/ITEpOAPD82fYB4UfKRdA1jraXsJJyTs+QFjc2WXBffgAg +X6m7Mp9nAMhRyXhULslO3trWFbFCa2dbQkDSyBRvsb2HZtztoLVyo1mtUg== +-----END RSA PRIVATE KEY-----` + privateKey, err := InitRSAPrivateKey(rsaPem) + require.NoError(t, err, "Failed to initialize RSA private key") + require.NotNil(t, privateKey) + t.Log(privateKey.PublicKey) + + publicPem, err := RSAPublicKeyToPEM(&privateKey.PublicKey) + require.NoError(t, err) + require.NotNil(t, publicPem) + t.Log(string(publicPem)) + + publicKey, err := PEMToRSAPublicKey(string(publicPem)) + require.NoError(t, err) + require.NotNil(t, publicKey) + t.Log(publicKey) +} diff --git a/pkg/util/os.go b/pkg/util/os.go new file mode 100644 index 0000000..77bab63 --- /dev/null +++ b/pkg/util/os.go @@ -0,0 +1,17 @@ +package util + +import "os" + +func GetMachineID() string { + machineID := os.Getenv("MACHINE_ID") + if machineID != "" { + return machineID + } + // On Linux, the machine ID is stored in /etc/machine-id + const machineIDPath = "/etc/machine-id" + data, err := os.ReadFile(machineIDPath) + if err != nil { + return "unknown-machine-id" + } + return string(data) +}