mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-19 04:26:39 +00:00
Feat: add namesapce create and list api (#2514)
Co-authored-by: barnettZQG <yiyun.pro>
This commit is contained in:
committed by
Hongchao Deng
parent
c7f9cdcbbf
commit
b85dda35f3
+180
-192
@@ -265,7 +265,7 @@
|
||||
}
|
||||
},
|
||||
"/api/v1/applications/{name}/components/{componentName}": {
|
||||
"post": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
"application/xml",
|
||||
"application/json"
|
||||
@@ -315,7 +315,7 @@
|
||||
"tags": [
|
||||
"application"
|
||||
],
|
||||
"summary": "deploy or update the application",
|
||||
"summary": "deploy or upgrade the application",
|
||||
"operationId": "deployApplication",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -329,7 +329,7 @@
|
||||
"responses": {
|
||||
"200": {
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.ApplicationBase"
|
||||
"$ref": "#/definitions/v1.ApplicationDeployRequest"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
@@ -466,6 +466,57 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"consumes": [
|
||||
"application/xml",
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/xml"
|
||||
],
|
||||
"tags": [
|
||||
"application"
|
||||
],
|
||||
"summary": "update policy for application",
|
||||
"operationId": "updateApplicationPolicy",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the application",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the application policy",
|
||||
"name": "policyName",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.UpdatePolicyRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.DetailPolicyResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"schema": {
|
||||
"$ref": "#/definitions/bcode.Bcode"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"consumes": [
|
||||
"application/xml",
|
||||
@@ -703,7 +754,7 @@
|
||||
"namespace"
|
||||
],
|
||||
"summary": "list all namespaces",
|
||||
"operationId": "noop",
|
||||
"operationId": "listNamespaces",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
@@ -723,7 +774,7 @@
|
||||
"namespace"
|
||||
],
|
||||
"summary": "create namespace",
|
||||
"operationId": "noop",
|
||||
"operationId": "createNamespace",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
@@ -741,155 +792,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/namespaces/{namespace}": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
"application/xml",
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/xml"
|
||||
],
|
||||
"tags": [
|
||||
"namespace"
|
||||
],
|
||||
"summary": "get one namespace",
|
||||
"operationId": "noop",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the namespace",
|
||||
"name": "namespace",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/namespaces/{namespace}/applications/:appname": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
"application/xml",
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/xml"
|
||||
],
|
||||
"tags": [
|
||||
"namespace"
|
||||
],
|
||||
"summary": "get the specified oam application in the specified namespace",
|
||||
"operationId": "noop",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the namespace",
|
||||
"name": "namespace",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the oam application",
|
||||
"name": "appname",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/xml",
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/xml"
|
||||
],
|
||||
"tags": [
|
||||
"namespace"
|
||||
],
|
||||
"summary": "create or update oam application in the specified namespace",
|
||||
"operationId": "noop",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the namespace",
|
||||
"name": "namespace",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the oam application",
|
||||
"name": "appname",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.ApplicationRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"consumes": [
|
||||
"application/xml",
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/xml"
|
||||
],
|
||||
"tags": [
|
||||
"namespace"
|
||||
],
|
||||
"summary": "create or update oam application in the specified namespace",
|
||||
"operationId": "noop",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the namespace",
|
||||
"name": "namespace",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the oam application",
|
||||
"name": "appname",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/policydefinitions": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
@@ -1399,11 +1301,11 @@
|
||||
},
|
||||
"common.AppRolloutStatus": {
|
||||
"required": [
|
||||
"rollingState",
|
||||
"upgradedReadyReplicas",
|
||||
"batchRollingState",
|
||||
"currentBatch",
|
||||
"upgradedReplicas",
|
||||
"batchRollingState",
|
||||
"upgradedReadyReplicas",
|
||||
"rollingState",
|
||||
"lastTargetAppRevision"
|
||||
],
|
||||
"properties": {
|
||||
@@ -1896,7 +1798,7 @@
|
||||
"$ref": "#/definitions/common.inputItem"
|
||||
}
|
||||
},
|
||||
"lables": {
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
@@ -2101,6 +2003,54 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.ApplicationDeployRequest": {
|
||||
"required": [
|
||||
"commit",
|
||||
"sourceType",
|
||||
"force"
|
||||
],
|
||||
"properties": {
|
||||
"commit": {
|
||||
"type": "string"
|
||||
},
|
||||
"force": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sourceType": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.ApplicationDeployResponse": {
|
||||
"required": [
|
||||
"version",
|
||||
"status",
|
||||
"reason",
|
||||
"deployUser",
|
||||
"commit",
|
||||
"sourceType"
|
||||
],
|
||||
"properties": {
|
||||
"commit": {
|
||||
"type": "string"
|
||||
},
|
||||
"deployUser": {
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string"
|
||||
},
|
||||
"sourceType": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.ApplicationRequest": {
|
||||
"required": [
|
||||
"components"
|
||||
@@ -2502,16 +2452,14 @@
|
||||
},
|
||||
"v1.CreateComponentRequest": {
|
||||
"required": [
|
||||
"appName",
|
||||
"name",
|
||||
"description",
|
||||
"icon",
|
||||
"componentType",
|
||||
"bindClusters"
|
||||
"bindClusters",
|
||||
"dependsOn"
|
||||
],
|
||||
"properties": {
|
||||
"appName": {
|
||||
"type": "string"
|
||||
},
|
||||
"bindClusters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -2521,9 +2469,18 @@
|
||||
"componentType": {
|
||||
"type": "string"
|
||||
},
|
||||
"dependsOn": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
@@ -2555,10 +2512,14 @@
|
||||
"v1.CreatePolicyRequest": {
|
||||
"required": [
|
||||
"name",
|
||||
"description",
|
||||
"type",
|
||||
"properties"
|
||||
],
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -2572,12 +2533,12 @@
|
||||
},
|
||||
"v1.DetailAddonResponse": {
|
||||
"required": [
|
||||
"name",
|
||||
"version",
|
||||
"description",
|
||||
"icon",
|
||||
"tags",
|
||||
"phase"
|
||||
"phase",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"deploy_data": {
|
||||
@@ -2615,13 +2576,13 @@
|
||||
"v1.DetailApplicationResponse": {
|
||||
"required": [
|
||||
"name",
|
||||
"updateTime",
|
||||
"description",
|
||||
"icon",
|
||||
"status",
|
||||
"namespace",
|
||||
"description",
|
||||
"createTime",
|
||||
"gatewayRule",
|
||||
"namespace",
|
||||
"createTime",
|
||||
"updateTime",
|
||||
"policies",
|
||||
"status",
|
||||
"resourceInfo",
|
||||
@@ -2688,12 +2649,12 @@
|
||||
},
|
||||
"v1.DetailClusterResponse": {
|
||||
"required": [
|
||||
"name",
|
||||
"description",
|
||||
"icon",
|
||||
"labels",
|
||||
"status",
|
||||
"reason",
|
||||
"name",
|
||||
"resourceInfo"
|
||||
],
|
||||
"properties": {
|
||||
@@ -2732,11 +2693,11 @@
|
||||
"v1.DetailComponentResponse": {
|
||||
"required": [
|
||||
"createTime",
|
||||
"updateTime",
|
||||
"appPrimaryKey",
|
||||
"name",
|
||||
"type",
|
||||
"creator"
|
||||
"name",
|
||||
"creator",
|
||||
"updateTime"
|
||||
],
|
||||
"properties": {
|
||||
"appPrimaryKey": {
|
||||
@@ -2770,7 +2731,7 @@
|
||||
"$ref": "#/definitions/common.inputItem"
|
||||
}
|
||||
},
|
||||
"lables": {
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
@@ -2811,17 +2772,25 @@
|
||||
},
|
||||
"v1.DetailPolicyResponse": {
|
||||
"required": [
|
||||
"name",
|
||||
"type",
|
||||
"description",
|
||||
"creator",
|
||||
"properties",
|
||||
"createTime",
|
||||
"updateTime"
|
||||
"updateTime",
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"properties": {
|
||||
"createTime": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"creator": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -2952,13 +2921,13 @@
|
||||
},
|
||||
"v1.ListNamespaceResponse": {
|
||||
"required": [
|
||||
"namesapces"
|
||||
"namespaces"
|
||||
],
|
||||
"properties": {
|
||||
"namesapces": {
|
||||
"namespaces": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v1.NamesapceBase"
|
||||
"$ref": "#/definitions/v1.NamespaceBase"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2994,7 +2963,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.NamesapceBase": {
|
||||
"v1.NamespaceBase": {
|
||||
"required": [
|
||||
"name",
|
||||
"description",
|
||||
@@ -3018,21 +2987,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.NamesapceDetailResponse": {
|
||||
"v1.NamespaceDetailResponse": {
|
||||
"required": [
|
||||
"name",
|
||||
"description",
|
||||
"createTime",
|
||||
"updateTime",
|
||||
"clusterBind"
|
||||
"updateTime"
|
||||
],
|
||||
"properties": {
|
||||
"clusterBind": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"createTime": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
@@ -3086,6 +3048,8 @@
|
||||
"required": [
|
||||
"name",
|
||||
"type",
|
||||
"description",
|
||||
"creator",
|
||||
"properties",
|
||||
"createTime",
|
||||
"updateTime"
|
||||
@@ -3095,6 +3059,12 @@
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"creator": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -3131,10 +3101,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.UpdatePolicyRequest": {
|
||||
"required": [
|
||||
"description",
|
||||
"type",
|
||||
"properties"
|
||||
],
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"properties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.UpdateWorkflowRequest": {
|
||||
"required": [
|
||||
"name",
|
||||
"namesapce",
|
||||
"namespace",
|
||||
"enable"
|
||||
],
|
||||
"properties": {
|
||||
@@ -3144,7 +3132,7 @@
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"namesapce": {
|
||||
"namespace": {
|
||||
"type": "string"
|
||||
},
|
||||
"steps": {
|
||||
|
||||
@@ -59,7 +59,7 @@ func New(ctx context.Context, cfg datastore.Config) (datastore.DataStore, error)
|
||||
Name: cfg.Database,
|
||||
Annotations: map[string]string{"description": "For kubevela apiserver metadata storage."},
|
||||
}}); err != nil {
|
||||
return nil, fmt.Errorf("create namesapce failure %w", err)
|
||||
return nil, fmt.Errorf("create namespace failure %w", err)
|
||||
}
|
||||
}
|
||||
return &kubeapi{
|
||||
|
||||
@@ -95,14 +95,14 @@ var _ = Describe("Test kubeapi datastore driver", func() {
|
||||
It("Test batch add funtion", func() {
|
||||
var datas = []datastore.Entity{
|
||||
&model.Application{Name: "kubevela-app-2", Description: "this is demo 2"},
|
||||
&model.Application{Namespace: "test-namesapce", Name: "kubevela-app-3", Description: "this is demo 3"},
|
||||
&model.Application{Namespace: "test-namesapce2", Name: "kubevela-app-4", Description: "this is demo 4"},
|
||||
&model.Application{Namespace: "test-namespace", Name: "kubevela-app-3", Description: "this is demo 3"},
|
||||
&model.Application{Namespace: "test-namespace2", Name: "kubevela-app-4", Description: "this is demo 4"},
|
||||
}
|
||||
err := kubeStore.BatchAdd(context.TODO(), datas)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
var datas2 = []datastore.Entity{
|
||||
&model.Application{Namespace: "test-namesapce", Name: "can-delete", Description: "this is demo can-delete"},
|
||||
&model.Application{Namespace: "test-namespace", Name: "can-delete", Description: "this is demo can-delete"},
|
||||
&model.Application{Name: "kubevela-app-2", Description: "this is demo 2"},
|
||||
}
|
||||
err = kubeStore.BatchAdd(context.TODO(), datas2)
|
||||
@@ -158,7 +158,7 @@ var _ = Describe("Test kubeapi datastore driver", func() {
|
||||
diff = cmp.Diff(len(list), 4)
|
||||
Expect(diff).Should(BeEmpty())
|
||||
|
||||
app.Namespace = "test-namesapce"
|
||||
app.Namespace = "test-namespace"
|
||||
list, err = kubeStore.List(context.TODO(), &app, nil)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
diff = cmp.Diff(len(list), 1)
|
||||
|
||||
@@ -63,14 +63,14 @@ var _ = Describe("Test mongodb datastore driver", func() {
|
||||
It("Test batch add funtion", func() {
|
||||
var datas = []datastore.Entity{
|
||||
&model.Application{Name: "kubevela-app-2", Description: "this is demo 2"},
|
||||
&model.Application{Namespace: "test-namesapce", Name: "kubevela-app-3", Description: "this is demo 3"},
|
||||
&model.Application{Namespace: "test-namesapce2", Name: "kubevela-app-4", Description: "this is demo 4"},
|
||||
&model.Application{Namespace: "test-namespace", Name: "kubevela-app-3", Description: "this is demo 3"},
|
||||
&model.Application{Namespace: "test-namespace2", Name: "kubevela-app-4", Description: "this is demo 4"},
|
||||
}
|
||||
err := mongodbDriver.BatchAdd(context.TODO(), datas)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
var datas2 = []datastore.Entity{
|
||||
&model.Application{Namespace: "test-namesapce", Name: "can-delete", Description: "this is demo can-delete"},
|
||||
&model.Application{Namespace: "test-namespace", Name: "can-delete", Description: "this is demo can-delete"},
|
||||
&model.Application{Name: "kubevela-app-2", Description: "this is demo 2"},
|
||||
}
|
||||
err = mongodbDriver.BatchAdd(context.TODO(), datas2)
|
||||
@@ -112,7 +112,7 @@ var _ = Describe("Test mongodb datastore driver", func() {
|
||||
diff = cmp.Diff(len(list), 4)
|
||||
Expect(diff).Should(BeEmpty())
|
||||
|
||||
app.Namespace = "test-namesapce"
|
||||
app.Namespace = "test-namespace"
|
||||
list, err = mongodbDriver.List(context.TODO(), &app, nil)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
diff = cmp.Diff(len(list), 1)
|
||||
|
||||
@@ -287,11 +287,11 @@ type ApplicationTemplateVersion struct {
|
||||
|
||||
// ListNamespaceResponse namesace list model
|
||||
type ListNamespaceResponse struct {
|
||||
Namespaces []NamesapceBase `json:"namesapces"`
|
||||
Namespaces []NamespaceBase `json:"namespaces"`
|
||||
}
|
||||
|
||||
// NamesapceBase namespace base model
|
||||
type NamesapceBase struct {
|
||||
// NamespaceBase namespace base model
|
||||
type NamespaceBase struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
@@ -304,10 +304,9 @@ type CreateNamespaceRequest struct {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// NamesapceDetailResponse namespace detail response
|
||||
type NamesapceDetailResponse struct {
|
||||
NamesapceBase
|
||||
ClusterBind map[string]string `json:"clusterBind"`
|
||||
// NamespaceDetailResponse namespace detail response
|
||||
type NamespaceDetailResponse struct {
|
||||
NamespaceBase
|
||||
}
|
||||
|
||||
// ListComponentDefinitionResponse list component dedinition response model
|
||||
@@ -382,7 +381,7 @@ type PolicyDefinition struct {
|
||||
// UpdateWorkflowRequest update or create application workflow
|
||||
type UpdateWorkflowRequest struct {
|
||||
Name string `json:"name" validate:"checkname"`
|
||||
Namespace string `json:"namesapce" validate:"checkname"`
|
||||
Namespace string `json:"namespace" validate:"checkname"`
|
||||
Steps []WorkflowStep `json:"steps,omitempty"`
|
||||
Enable bool `json:"enable"`
|
||||
}
|
||||
|
||||
@@ -68,7 +68,10 @@ type applicationUsecaseImpl struct {
|
||||
|
||||
// NewApplicationUsecase new application usecase
|
||||
func NewApplicationUsecase(ds datastore.DataStore, workflowUsecase WorkflowUsecase) ApplicationUsecase {
|
||||
kubecli, _ := clients.GetKubeClient()
|
||||
kubecli, err := clients.GetKubeClient()
|
||||
if err != nil {
|
||||
log.Logger.Fatalf("get kubeclient failure %s", err.Error())
|
||||
}
|
||||
return &applicationUsecaseImpl{
|
||||
ds: ds,
|
||||
workflowUsecase: workflowUsecase,
|
||||
@@ -433,7 +436,7 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat
|
||||
if err := c.kubeClient.Get(ctx, types.NamespacedName{Name: oamApp.Namespace}, &namespace); apierrors.IsNotFound(err) {
|
||||
namespace.Name = oamApp.Namespace
|
||||
if err := c.kubeClient.Create(ctx, &namespace); err != nil {
|
||||
log.Logger.Errorf("auto create namesapce failure %s", err.Error())
|
||||
log.Logger.Errorf("auto create namespace failure %s", err.Error())
|
||||
return nil, bcode.ErrCreateNamespace
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
Copyright 2021 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/clients"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/log"
|
||||
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
|
||||
)
|
||||
|
||||
// NamespaceUsecase namespace manage usecase.
|
||||
// Namespace acts as the tenant isolation model on the control side.
|
||||
type NamespaceUsecase interface {
|
||||
ListNamespaces(ctx context.Context) ([]apisv1.NamespaceBase, error)
|
||||
CreateNamespace(ctx context.Context, req apisv1.CreateNamespaceRequest) (*apisv1.NamespaceBase, error)
|
||||
}
|
||||
|
||||
// AnnotationDescription set namespace description in annotation
|
||||
const AnnotationDescription string = "description"
|
||||
|
||||
// LabelCreator set namesapce creator in labels
|
||||
const LabelCreator string = "creator"
|
||||
|
||||
type namespaceUsecaseImpl struct {
|
||||
kubeClient client.Client
|
||||
}
|
||||
|
||||
// NewNamespaceUsecase new namespace usecase
|
||||
func NewNamespaceUsecase() NamespaceUsecase {
|
||||
kubecli, err := clients.GetKubeClient()
|
||||
if err != nil {
|
||||
log.Logger.Fatalf("get kubeclient failure %s", err.Error())
|
||||
}
|
||||
return &namespaceUsecaseImpl{kubeClient: kubecli}
|
||||
}
|
||||
|
||||
// ListNamespaces list controller cluster namespaces
|
||||
func (n *namespaceUsecaseImpl) ListNamespaces(ctx context.Context) ([]apisv1.NamespaceBase, error) {
|
||||
|
||||
// TODO: Consider whether to query only namespaces created by Vela
|
||||
var kubeNamespaces corev1.NamespaceList
|
||||
if err := n.kubeClient.List(ctx, &kubeNamespaces, &client.ListOptions{}); err != nil {
|
||||
log.Logger.Errorf("query namespace list from cluster failure %s", err.Error())
|
||||
return nil, bcode.ErrNamespaceQuery
|
||||
}
|
||||
var namespaces []apisv1.NamespaceBase
|
||||
for _, namesapce := range kubeNamespaces.Items {
|
||||
namespaces = append(namespaces, apisv1.NamespaceBase{
|
||||
Name: namesapce.Name,
|
||||
Description: namesapce.Annotations[AnnotationDescription],
|
||||
CreateTime: namesapce.CreationTimestamp.Time,
|
||||
UpdateTime: namesapce.CreationTimestamp.Time,
|
||||
})
|
||||
}
|
||||
return namespaces, nil
|
||||
}
|
||||
|
||||
// CreateNamespace create namespace to controller cluster
|
||||
func (n *namespaceUsecaseImpl) CreateNamespace(ctx context.Context, req apisv1.CreateNamespaceRequest) (*apisv1.NamespaceBase, error) {
|
||||
if err := n.kubeClient.Create(ctx, &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: req.Name,
|
||||
Labels: map[string]string{
|
||||
LabelCreator: "kubevela",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
AnnotationDescription: req.Description,
|
||||
},
|
||||
},
|
||||
Spec: corev1.NamespaceSpec{},
|
||||
}); err != nil {
|
||||
if apierrors.IsAlreadyExists(err) {
|
||||
return nil, bcode.ErrNamespaceIsExist
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &apisv1.NamespaceBase{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright 2021 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
|
||||
)
|
||||
|
||||
var _ = Describe("Test namespace usecase functions", func() {
|
||||
var (
|
||||
namespaceUsecase *namespaceUsecaseImpl
|
||||
)
|
||||
BeforeEach(func() {
|
||||
namespaceUsecase = &namespaceUsecaseImpl{kubeClient: k8sClient}
|
||||
})
|
||||
It("Test CreateNamespace function", func() {
|
||||
req := apisv1.CreateNamespaceRequest{
|
||||
Name: "test-namespace",
|
||||
Description: "this is a namespace description 王二",
|
||||
}
|
||||
base, err := namespaceUsecase.CreateNamespace(context.TODO(), req)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(cmp.Diff(base.Description, req.Description)).Should(BeEmpty())
|
||||
})
|
||||
|
||||
It("Test ListNamespace function", func() {
|
||||
_, err := namespaceUsecase.ListNamespaces(context.TODO())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
Copyright 2021 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package bcode
|
||||
|
||||
// ErrNamespaceQuery query namespace failure from k8s api
|
||||
var ErrNamespaceQuery = NewBcode(500, 30001, "query namespace list from cluster failure")
|
||||
|
||||
// ErrNamespaceIsExist namespace name is exist
|
||||
var ErrNamespaceIsExist = NewBcode(400, 30002, "namespace name is exist")
|
||||
@@ -20,13 +20,22 @@ import (
|
||||
restfulspec "github.com/emicklei/go-restful-openapi/v2"
|
||||
restful "github.com/emicklei/go-restful/v3"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/log"
|
||||
apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/usecase"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
|
||||
)
|
||||
|
||||
type namespaceWebService struct {
|
||||
namespaceUsecase usecase.NamespaceUsecase
|
||||
}
|
||||
|
||||
func (c *namespaceWebService) GetWebService() *restful.WebService {
|
||||
// NewNamespaceWebService new namespace webservice
|
||||
func NewNamespaceWebService(namespaceUsecase usecase.NamespaceUsecase) WebService {
|
||||
return &namespaceWebService{namespaceUsecase: namespaceUsecase}
|
||||
}
|
||||
|
||||
func (n *namespaceWebService) GetWebService() *restful.WebService {
|
||||
ws := new(restful.WebService)
|
||||
ws.Path(versionPrefix+"/namespaces").
|
||||
Consumes(restful.MIME_XML, restful.MIME_JSON).
|
||||
@@ -35,42 +44,53 @@ func (c *namespaceWebService) GetWebService() *restful.WebService {
|
||||
|
||||
tags := []string{"namespace"}
|
||||
|
||||
ws.Route(ws.GET("/").To(noop).
|
||||
ws.Route(ws.GET("/").To(n.listNamespaces).
|
||||
Doc("list all namespaces").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Writes(apis.ListNamespaceResponse{}))
|
||||
|
||||
ws.Route(ws.POST("/").To(noop).
|
||||
ws.Route(ws.POST("/").To(n.createNamespace).
|
||||
Doc("create namespace").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Reads(apis.CreateNamespaceRequest{}).
|
||||
Writes(apis.NamesapceDetailResponse{}))
|
||||
|
||||
ws.Route(ws.GET("/{namespace}").To(noop).
|
||||
Doc("get one namespace").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")).
|
||||
Writes(apis.NamesapceDetailResponse{}))
|
||||
|
||||
// Compatible with historical apis
|
||||
ws.Route(ws.GET("/{namespace}/applications/:appname").To(noop).
|
||||
Doc("get the specified oam application in the specified namespace").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")).
|
||||
Param(ws.PathParameter("appname", "identifier of the oam application").DataType("string")).
|
||||
Writes(apis.ApplicationResponse{}))
|
||||
|
||||
ws.Route(ws.POST("/{namespace}/applications/:appname").To(noop).
|
||||
Doc("create or update oam application in the specified namespace").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")).
|
||||
Param(ws.PathParameter("appname", "identifier of the oam application").DataType("string")).
|
||||
Reads(apis.ApplicationRequest{}))
|
||||
|
||||
ws.Route(ws.DELETE("/{namespace}/applications/:appname").To(noop).
|
||||
Doc("create or update oam application in the specified namespace").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")).
|
||||
Param(ws.PathParameter("appname", "identifier of the oam application").DataType("string")))
|
||||
Writes(apis.NamespaceDetailResponse{}))
|
||||
return ws
|
||||
}
|
||||
|
||||
func (n *namespaceWebService) listNamespaces(req *restful.Request, res *restful.Response) {
|
||||
namespaces, err := n.namespaceUsecase.ListNamespaces(req.Request.Context())
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
if err := res.WriteEntity(apis.ListNamespaceResponse{Namespaces: namespaces}); err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (n *namespaceWebService) createNamespace(req *restful.Request, res *restful.Response) {
|
||||
// Verify the validity of parameters
|
||||
var createReq apis.CreateNamespaceRequest
|
||||
if err := req.ReadEntity(&createReq); err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
if err := validate.Struct(&createReq); err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
// Call the usecase layer code
|
||||
namespaceBase, err := n.namespaceUsecase.CreateNamespace(req.Request.Context(), createReq)
|
||||
if err != nil {
|
||||
log.Logger.Errorf("create application failure %s", err.Error())
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Write back response data
|
||||
if err := res.WriteEntity(apis.NamespaceDetailResponse{NamespaceBase: *namespaceBase}); err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,26 +29,26 @@ var _ = Describe("Test validate function", func() {
|
||||
Expect(cmp.Diff(nameRegexp.MatchString("///Asd asda "), false)).Should(BeEmpty())
|
||||
var app0 = apisv1.CreateApplicationRequest{
|
||||
Name: "a",
|
||||
Namespace: "namesapce",
|
||||
Namespace: "namespace",
|
||||
}
|
||||
err := validate.Struct(&app0)
|
||||
Expect(err).ShouldNot(BeNil())
|
||||
var app1 = apisv1.CreateApplicationRequest{
|
||||
Name: "Asdasd",
|
||||
Namespace: "namesapce",
|
||||
Namespace: "namespace",
|
||||
}
|
||||
err = validate.Struct(&app1)
|
||||
Expect(err).ShouldNot(BeNil())
|
||||
var app2 = apisv1.CreateApplicationRequest{
|
||||
Name: "asdasd asdasd ++",
|
||||
Namespace: "namesapce",
|
||||
Namespace: "namespace",
|
||||
}
|
||||
err = validate.Struct(&app2)
|
||||
Expect(err).ShouldNot(BeNil())
|
||||
|
||||
var app3 = apisv1.CreateApplicationRequest{
|
||||
Name: "asdasd",
|
||||
Namespace: "namesapce",
|
||||
Namespace: "namespace",
|
||||
}
|
||||
err = validate.Struct(&app3)
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
@@ -62,10 +62,11 @@ func Init(ctx context.Context, ds datastore.DataStore) {
|
||||
clusterUsecase := usecase.NewClusterUsecase(ds)
|
||||
workflowUsecase := usecase.NewWorkflowUsecase(ds)
|
||||
applicationUsecase := usecase.NewApplicationUsecase(ds, workflowUsecase)
|
||||
namespaceUsecase := usecase.NewNamespaceUsecase()
|
||||
oamApplicationUsecase := usecase.NewOAMApplicationUsecase()
|
||||
RegistWebService(NewClusterWebService(clusterUsecase))
|
||||
RegistWebService(NewApplicationWebService(applicationUsecase))
|
||||
RegistWebService(&namespaceWebService{})
|
||||
RegistWebService(NewNamespaceWebService(namespaceUsecase))
|
||||
RegistWebService(&componentDefinitionWebservice{})
|
||||
RegistWebService(&addonWebService{})
|
||||
RegistWebService(NewOAMApplication(oamApplicationUsecase))
|
||||
|
||||
@@ -209,7 +209,7 @@ func (a *AppManifestsDispatcher) retrieveLegacyResourceTrackers(ctx context.Cont
|
||||
oldRtList := &v1beta1.ResourceTrackerList{}
|
||||
if err := a.c.List(ctx, oldRtList, client.MatchingLabels{
|
||||
oam.LabelAppName: ExtractAppName(a.currentRTName, a.namespace),
|
||||
"app.oam.dev/namesapce": a.namespace,
|
||||
"app.oam.dev/namespace": a.namespace,
|
||||
}); err != nil {
|
||||
return errors.Wrap(err, "cannot retrieve legacy resource trackers with miss-spell label")
|
||||
}
|
||||
|
||||
@@ -490,7 +490,7 @@ var _ = Describe("Test compatibility code", func() {
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: appName + "-v2-" + namespaceName,
|
||||
Labels: map[string]string{
|
||||
"app.oam.dev/namesapce": namespaceName,
|
||||
"app.oam.dev/namespace": namespaceName,
|
||||
oam.LabelAppName: appName,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -245,7 +245,7 @@ func NewFakeRecorder(bufferSize int) *FakeRecorder {
|
||||
|
||||
// randomNamespaceName generates a random name based on the basic name.
|
||||
// Running each ginkgo case in a new namespace with a random name can avoid
|
||||
// waiting a long time to GC namesapce.
|
||||
// waiting a long time to GC namespace.
|
||||
func randomNamespaceName(basic string) string {
|
||||
return fmt.Sprintf("%s-%s", basic, strconv.FormatInt(rand.Int63(), 16))
|
||||
}
|
||||
|
||||
@@ -1338,7 +1338,7 @@ func TestGetDefinitionWithClusterScope(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
// old cluster workload trait scope definition crd is cluster scope, the namesapce field is empty
|
||||
// old cluster workload trait scope definition crd is cluster scope, the namespace field is empty
|
||||
noNs := v1alpha2.TraitDefinition{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "noNsDefinition",
|
||||
|
||||
@@ -38,7 +38,7 @@ var _ = Describe("Test application rest api", func() {
|
||||
defer GinkgoRecover()
|
||||
var req = apisv1.CreateApplicationRequest{
|
||||
Name: "test-app-sadasd",
|
||||
Namespace: "test-app-namesapce",
|
||||
Namespace: "test-app-namespace",
|
||||
Description: "this is a test app",
|
||||
Icon: "",
|
||||
Labels: map[string]string{"test": "true"},
|
||||
@@ -77,7 +77,7 @@ var _ = Describe("Test application rest api", func() {
|
||||
Expect(err).Should(Succeed())
|
||||
var req = apisv1.CreateApplicationRequest{
|
||||
Name: "test-app-sadasd",
|
||||
Namespace: "test-app-namesapce",
|
||||
Namespace: "test-app-namespace",
|
||||
Description: "this is a test app",
|
||||
Icon: "",
|
||||
Labels: map[string]string{"test": "true"},
|
||||
@@ -177,7 +177,7 @@ var _ = Describe("Test application rest api", func() {
|
||||
Expect(cmp.Diff(response.Status, model.DeployEventRunning)).Should(BeEmpty())
|
||||
|
||||
var oam v1beta1.Application
|
||||
err = k8sClient.Get(context.TODO(), types.NamespacedName{Name: "test-app-sadasd", Namespace: "test-app-namesapce"}, &oam)
|
||||
err = k8sClient.Get(context.TODO(), types.NamespacedName{Name: "test-app-sadasd", Namespace: "test-app-namespace"}, &oam)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(cmp.Diff(len(oam.Spec.Components), 2)).Should(BeEmpty())
|
||||
Expect(cmp.Diff(len(oam.Spec.Policies), 1)).Should(BeEmpty())
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
Copyright 2021 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package e2e_apiserver_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
|
||||
)
|
||||
|
||||
var _ = Describe("Test namespace rest api", func() {
|
||||
It("Test create namespace", func() {
|
||||
defer GinkgoRecover()
|
||||
var req = apisv1.CreateNamespaceRequest{
|
||||
Name: "dev-team",
|
||||
Description: "开发环境租户",
|
||||
}
|
||||
bodyByte, err := json.Marshal(req)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
res, err := http.Post("http://127.0.0.1:8000/api/v1/namespaces", "application/json", bytes.NewBuffer(bodyByte))
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(res).ShouldNot(BeNil())
|
||||
Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty())
|
||||
Expect(res.Body).ShouldNot(BeNil())
|
||||
defer res.Body.Close()
|
||||
var namespaceBase apisv1.NamespaceBase
|
||||
err = json.NewDecoder(res.Body).Decode(&namespaceBase)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(cmp.Diff(namespaceBase.Name, req.Name)).Should(BeEmpty())
|
||||
Expect(cmp.Diff(namespaceBase.Description, req.Description)).Should(BeEmpty())
|
||||
})
|
||||
|
||||
It("Test list namespace", func() {
|
||||
defer GinkgoRecover()
|
||||
res, err := http.Get("http://127.0.0.1:8000/api/v1/namespaces")
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(res).ShouldNot(BeNil())
|
||||
Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty())
|
||||
Expect(res.Body).ShouldNot(BeNil())
|
||||
defer res.Body.Close()
|
||||
var namespaces apisv1.ListNamespaceResponse
|
||||
err = json.NewDecoder(res.Body).Decode(&namespaces)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
@@ -302,7 +302,7 @@ func RequestReconcileNow(ctx context.Context, o client.Object) {
|
||||
|
||||
// randomNamespaceName generates a random name based on the basic name.
|
||||
// Running each ginkgo case in a new namespace with a random name can avoid
|
||||
// waiting a long time to GC namesapce.
|
||||
// waiting a long time to GC namespace.
|
||||
func randomNamespaceName(basic string) string {
|
||||
return fmt.Sprintf("%s-%s", basic, strconv.FormatInt(rand.Int63(), 16))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user