diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c3e261fd9..1a9808143 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,16 +27,5 @@ jobs: - name: go mod vendor run: go mod vendor - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - - name: Build Docker images - uses: docker/build-push-action@v2 - with: - context: . - platforms: linux/amd64,linux/arm64 - file: Dockerfile - push: false \ No newline at end of file + - name: go build ob-operator + run: go build -o bin/manager cmd/main.go \ No newline at end of file diff --git a/distribution/dashboard/bindata/bindata.go b/distribution/dashboard/bindata/bindata.go index 494835f54..97656305c 100644 --- a/distribution/dashboard/bindata/bindata.go +++ b/distribution/dashboard/bindata/bindata.go @@ -200,11 +200,13 @@ var _bindata = map[string]func() (*asset, error){ // directory embedded in the file by go-bindata. // For example if you run go-bindata on data/... and data contains the // following hierarchy: -// data/ -// foo.txt -// img/ -// a.png -// b.png +// +// data/ +// foo.txt +// img/ +// a.png +// b.png +// // then AssetDir("data") would return []string{"foo.txt", "img"} // AssetDir("data/img") would return []string{"a.png", "b.png"} // AssetDir("foo.txt") and AssetDir("notexist") would return an error @@ -237,10 +239,10 @@ type bintree struct { } var _bintree = &bintree{nil, map[string]*bintree{ - "assets": &bintree{nil, map[string]*bintree{ - "metric_en_US.yaml": &bintree{assetsMetric_en_usYaml, map[string]*bintree{}}, - "metric_expr.yaml": &bintree{assetsMetric_exprYaml, map[string]*bintree{}}, - "metric_zh_CN.yaml": &bintree{assetsMetric_zh_cnYaml, map[string]*bintree{}}, + "assets": {nil, map[string]*bintree{ + "metric_en_US.yaml": {assetsMetric_en_usYaml, map[string]*bintree{}}, + "metric_expr.yaml": {assetsMetric_exprYaml, map[string]*bintree{}}, + "metric_zh_CN.yaml": {assetsMetric_zh_cnYaml, map[string]*bintree{}}, }}, }} diff --git a/distribution/dashboard/docs/docs.go b/distribution/dashboard/docs/docs.go index 36d422125..4ca61073d 100644 --- a/distribution/dashboard/docs/docs.go +++ b/distribution/dashboard/docs/docs.go @@ -1324,20 +1324,6 @@ const docTemplate = `{ "summary": "Create tenant", "operationId": "CreateTenant", "parameters": [ - { - "type": "string", - "description": "obtenant namespace", - "name": "namespace", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "obtenant name", - "name": "name", - "in": "path", - "required": true - }, { "description": "create obtenant request body", "name": "body", @@ -1860,13 +1846,13 @@ const docTemplate = `{ } } }, - "post": { + "delete": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Update backup policy of specific tenant", + "description": "Delete backup policy of specific tenant", "consumes": [ "application/json" ], @@ -1876,8 +1862,8 @@ const docTemplate = `{ "tags": [ "Obtenant" ], - "summary": "Update backup policy of specific tenant", - "operationId": "UpdateBackupPolicy", + "summary": "Delete backup policy of specific tenant", + "operationId": "DeleteBackupPolicy", "parameters": [ { "type": "string", @@ -1892,34 +1878,13 @@ const docTemplate = `{ "name": "name", "in": "path", "required": true - }, - { - "description": "update backup policy request body", - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/param.UpdateBackupPolicy" - } } ], "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.BackupPolicy" - } - } - } - ] + "$ref": "#/definitions/response.APIResponse" } }, "400": { @@ -1942,13 +1907,13 @@ const docTemplate = `{ } } }, - "delete": { + "patch": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Delete backup policy of specific tenant", + "description": "Update backup policy of specific tenant", "consumes": [ "application/json" ], @@ -1958,8 +1923,8 @@ const docTemplate = `{ "tags": [ "Obtenant" ], - "summary": "Delete backup policy of specific tenant", - "operationId": "DeleteBackupPolicy", + "summary": "Update backup policy of specific tenant", + "operationId": "UpdateBackupPolicy", "parameters": [ { "type": "string", @@ -1974,13 +1939,34 @@ const docTemplate = `{ "name": "name", "in": "path", "required": true + }, + { + "description": "update backup policy request body", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/param.UpdateBackupPolicy" + } } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/response.APIResponse" + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.BackupPolicy" + } + } + } + ] } }, "400": { @@ -2368,30 +2354,26 @@ const docTemplate = `{ }, "param.ChangeTenantRole": { "type": "object", - "required": [ - "tenantRole" - ], "properties": { - "switchover": { + "failover": { "type": "boolean" }, - "tenantRole": { - "description": "Enum: Primary, Standby", - "type": "string" + "switchover": { + "type": "boolean" } } }, "param.ChangeUserPassword": { "type": "object", "required": [ - "Password", - "User" + "password", + "user" ], "properties": { - "Password": { + "password": { "type": "string" }, - "User": { + "user": { "description": "Description: The user name of the database account, only root is supported now.", "type": "string" } @@ -2400,8 +2382,9 @@ const docTemplate = `{ "param.CreateBackupPolicy": { "type": "object", "required": [ - "destType", - "scheduleType" + "archivePath", + "bakDataPath", + "destType" ], "properties": { "archivePath": { @@ -2411,26 +2394,33 @@ const docTemplate = `{ "type": "string" }, "bakEncryptionPassword": { - "type": "string" + "type": "string", + "example": "encryptedPassword" }, "destType": { "description": "Enum: NFS, OSS", - "type": "string" + "type": "string", + "example": "NFS" }, - "jobKeepWindow": { - "type": "string" + "jobKeepDays": { + "type": "integer", + "example": 5 }, "ossAccessId": { - "type": "string" + "type": "string", + "example": "encryptedPassword" }, "ossAccessKey": { - "type": "string" + "type": "string", + "example": "encryptedPassword" }, - "pieceInterval": { - "type": "string" + "pieceIntervalDays": { + "type": "integer", + "example": 1 }, - "recoveryWindow": { - "type": "string" + "recoveryDays": { + "type": "integer", + "example": 3 }, "scheduleDates": { "type": "array", @@ -2439,11 +2429,14 @@ const docTemplate = `{ } }, "scheduleTime": { - "description": "Description: HH:MM", - "type": "string" + "description": "Description: HH:MM\nExample: 04:00", + "type": "string", + "example": "04:00" }, "scheduleType": { - "type": "string" + "description": "Enum: Weekly, Monthly", + "type": "string", + "example": "Weekly" } } }, @@ -2685,7 +2678,8 @@ const docTemplate = `{ "type": "object", "properties": { "timestamp": { - "type": "string" + "type": "string", + "example": "2024-02-23 17:47:00" }, "unlimited": { "type": "boolean" @@ -2712,6 +2706,11 @@ const docTemplate = `{ }, "param.RestoreSourceSpec": { "type": "object", + "required": [ + "archiveSource", + "bakDataSource", + "type" + ], "properties": { "archiveSource": { "type": "string" @@ -2741,7 +2740,8 @@ const docTemplate = `{ "type": "object", "properties": { "timestamp": { - "type": "string" + "type": "string", + "example": "2024-02-23 17:47:00" }, "unlimited": { "type": "boolean" @@ -2765,10 +2765,13 @@ const docTemplate = `{ "properties": { "backupType": { "description": "Enum: Full, Incremental", - "type": "string" + "type": "string", + "example": "Full" }, "day": { - "type": "integer" + "description": "Description: 1-31 for monthly, 1-7 for weekly", + "type": "integer", + "example": 3 } } }, @@ -2812,18 +2815,18 @@ const docTemplate = `{ }, "param.UpdateBackupPolicy": { "type": "object", - "required": [ - "status" - ], "properties": { "jobKeepWindow": { - "type": "string" + "type": "integer", + "example": 5 }, "pieceInterval": { - "type": "string" + "type": "integer", + "example": 1 }, "recoveryWindow": { - "type": "string" + "type": "integer", + "example": 3 }, "scheduleDates": { "type": "array", @@ -2831,12 +2834,18 @@ const docTemplate = `{ "$ref": "#/definitions/param.ScheduleDate" } }, + "scheduleTime": { + "description": "Description: HH:MM\nExample: 04:00", + "type": "string", + "example": "04:00" + }, "scheduleType": { "description": "Enum: Weekly, Monthly", - "type": "string" + "type": "string", + "example": "Weekly" }, "status": { - "description": "Enum: Paused, Running", + "description": "Enum: PAUSED, RUNNING", "type": "string" } } @@ -2923,8 +2932,9 @@ const docTemplate = `{ "response.BackupPolicy": { "type": "object", "required": [ - "destType", - "scheduleType" + "archivePath", + "bakDataPath", + "destType" ], "properties": { "archivePath": { @@ -2938,10 +2948,12 @@ const docTemplate = `{ }, "destType": { "description": "Enum: NFS, OSS", - "type": "string" + "type": "string", + "example": "NFS" }, - "jobKeepWindow": { - "type": "string" + "jobKeepDays": { + "type": "integer", + "example": 5 }, "name": { "type": "string" @@ -2952,11 +2964,13 @@ const docTemplate = `{ "ossAccessSecret": { "type": "string" }, - "pieceInterval": { - "type": "string" + "pieceIntervalDays": { + "type": "integer", + "example": 1 }, - "recoveryWindow": { - "type": "string" + "recoveryDays": { + "type": "integer", + "example": 3 }, "scheduleDates": { "type": "array", @@ -2965,11 +2979,14 @@ const docTemplate = `{ } }, "scheduleTime": { - "description": "Description: HH:MM", - "type": "string" + "description": "Description: HH:MM\nExample: 04:00", + "type": "string", + "example": "04:00" }, "scheduleType": { - "type": "string" + "description": "Enum: Weekly, Monthly", + "type": "string", + "example": "Weekly" }, "status": { "type": "string" diff --git a/distribution/dashboard/docs/swagger.json b/distribution/dashboard/docs/swagger.json index 4d6c46b76..f682ef5fb 100644 --- a/distribution/dashboard/docs/swagger.json +++ b/distribution/dashboard/docs/swagger.json @@ -1317,20 +1317,6 @@ "summary": "Create tenant", "operationId": "CreateTenant", "parameters": [ - { - "type": "string", - "description": "obtenant namespace", - "name": "namespace", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "obtenant name", - "name": "name", - "in": "path", - "required": true - }, { "description": "create obtenant request body", "name": "body", @@ -1853,13 +1839,13 @@ } } }, - "post": { + "delete": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Update backup policy of specific tenant", + "description": "Delete backup policy of specific tenant", "consumes": [ "application/json" ], @@ -1869,8 +1855,8 @@ "tags": [ "Obtenant" ], - "summary": "Update backup policy of specific tenant", - "operationId": "UpdateBackupPolicy", + "summary": "Delete backup policy of specific tenant", + "operationId": "DeleteBackupPolicy", "parameters": [ { "type": "string", @@ -1885,34 +1871,13 @@ "name": "name", "in": "path", "required": true - }, - { - "description": "update backup policy request body", - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/param.UpdateBackupPolicy" - } } ], "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.BackupPolicy" - } - } - } - ] + "$ref": "#/definitions/response.APIResponse" } }, "400": { @@ -1935,13 +1900,13 @@ } } }, - "delete": { + "patch": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Delete backup policy of specific tenant", + "description": "Update backup policy of specific tenant", "consumes": [ "application/json" ], @@ -1951,8 +1916,8 @@ "tags": [ "Obtenant" ], - "summary": "Delete backup policy of specific tenant", - "operationId": "DeleteBackupPolicy", + "summary": "Update backup policy of specific tenant", + "operationId": "UpdateBackupPolicy", "parameters": [ { "type": "string", @@ -1967,13 +1932,34 @@ "name": "name", "in": "path", "required": true + }, + { + "description": "update backup policy request body", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/param.UpdateBackupPolicy" + } } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/response.APIResponse" + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.BackupPolicy" + } + } + } + ] } }, "400": { @@ -2361,30 +2347,26 @@ }, "param.ChangeTenantRole": { "type": "object", - "required": [ - "tenantRole" - ], "properties": { - "switchover": { + "failover": { "type": "boolean" }, - "tenantRole": { - "description": "Enum: Primary, Standby", - "type": "string" + "switchover": { + "type": "boolean" } } }, "param.ChangeUserPassword": { "type": "object", "required": [ - "Password", - "User" + "password", + "user" ], "properties": { - "Password": { + "password": { "type": "string" }, - "User": { + "user": { "description": "Description: The user name of the database account, only root is supported now.", "type": "string" } @@ -2393,8 +2375,9 @@ "param.CreateBackupPolicy": { "type": "object", "required": [ - "destType", - "scheduleType" + "archivePath", + "bakDataPath", + "destType" ], "properties": { "archivePath": { @@ -2404,26 +2387,33 @@ "type": "string" }, "bakEncryptionPassword": { - "type": "string" + "type": "string", + "example": "encryptedPassword" }, "destType": { "description": "Enum: NFS, OSS", - "type": "string" + "type": "string", + "example": "NFS" }, - "jobKeepWindow": { - "type": "string" + "jobKeepDays": { + "type": "integer", + "example": 5 }, "ossAccessId": { - "type": "string" + "type": "string", + "example": "encryptedPassword" }, "ossAccessKey": { - "type": "string" + "type": "string", + "example": "encryptedPassword" }, - "pieceInterval": { - "type": "string" + "pieceIntervalDays": { + "type": "integer", + "example": 1 }, - "recoveryWindow": { - "type": "string" + "recoveryDays": { + "type": "integer", + "example": 3 }, "scheduleDates": { "type": "array", @@ -2432,11 +2422,14 @@ } }, "scheduleTime": { - "description": "Description: HH:MM", - "type": "string" + "description": "Description: HH:MM\nExample: 04:00", + "type": "string", + "example": "04:00" }, "scheduleType": { - "type": "string" + "description": "Enum: Weekly, Monthly", + "type": "string", + "example": "Weekly" } } }, @@ -2678,7 +2671,8 @@ "type": "object", "properties": { "timestamp": { - "type": "string" + "type": "string", + "example": "2024-02-23 17:47:00" }, "unlimited": { "type": "boolean" @@ -2705,6 +2699,11 @@ }, "param.RestoreSourceSpec": { "type": "object", + "required": [ + "archiveSource", + "bakDataSource", + "type" + ], "properties": { "archiveSource": { "type": "string" @@ -2734,7 +2733,8 @@ "type": "object", "properties": { "timestamp": { - "type": "string" + "type": "string", + "example": "2024-02-23 17:47:00" }, "unlimited": { "type": "boolean" @@ -2758,10 +2758,13 @@ "properties": { "backupType": { "description": "Enum: Full, Incremental", - "type": "string" + "type": "string", + "example": "Full" }, "day": { - "type": "integer" + "description": "Description: 1-31 for monthly, 1-7 for weekly", + "type": "integer", + "example": 3 } } }, @@ -2805,18 +2808,18 @@ }, "param.UpdateBackupPolicy": { "type": "object", - "required": [ - "status" - ], "properties": { "jobKeepWindow": { - "type": "string" + "type": "integer", + "example": 5 }, "pieceInterval": { - "type": "string" + "type": "integer", + "example": 1 }, "recoveryWindow": { - "type": "string" + "type": "integer", + "example": 3 }, "scheduleDates": { "type": "array", @@ -2824,12 +2827,18 @@ "$ref": "#/definitions/param.ScheduleDate" } }, + "scheduleTime": { + "description": "Description: HH:MM\nExample: 04:00", + "type": "string", + "example": "04:00" + }, "scheduleType": { "description": "Enum: Weekly, Monthly", - "type": "string" + "type": "string", + "example": "Weekly" }, "status": { - "description": "Enum: Paused, Running", + "description": "Enum: PAUSED, RUNNING", "type": "string" } } @@ -2916,8 +2925,9 @@ "response.BackupPolicy": { "type": "object", "required": [ - "destType", - "scheduleType" + "archivePath", + "bakDataPath", + "destType" ], "properties": { "archivePath": { @@ -2931,10 +2941,12 @@ }, "destType": { "description": "Enum: NFS, OSS", - "type": "string" + "type": "string", + "example": "NFS" }, - "jobKeepWindow": { - "type": "string" + "jobKeepDays": { + "type": "integer", + "example": 5 }, "name": { "type": "string" @@ -2945,11 +2957,13 @@ "ossAccessSecret": { "type": "string" }, - "pieceInterval": { - "type": "string" + "pieceIntervalDays": { + "type": "integer", + "example": 1 }, - "recoveryWindow": { - "type": "string" + "recoveryDays": { + "type": "integer", + "example": 3 }, "scheduleDates": { "type": "array", @@ -2958,11 +2972,14 @@ } }, "scheduleTime": { - "description": "Description: HH:MM", - "type": "string" + "description": "Description: HH:MM\nExample: 04:00", + "type": "string", + "example": "04:00" }, "scheduleType": { - "type": "string" + "description": "Enum: Weekly, Monthly", + "type": "string", + "example": "Weekly" }, "status": { "type": "string" diff --git a/distribution/dashboard/docs/swagger.yaml b/distribution/dashboard/docs/swagger.yaml index cfefe91d7..83801c24b 100644 --- a/distribution/dashboard/docs/swagger.yaml +++ b/distribution/dashboard/docs/swagger.yaml @@ -23,25 +23,22 @@ definitions: type: object param.ChangeTenantRole: properties: + failover: + type: boolean switchover: type: boolean - tenantRole: - description: 'Enum: Primary, Standby' - type: string - required: - - tenantRole type: object param.ChangeUserPassword: properties: - Password: + password: type: string - User: + user: description: 'Description: The user name of the database account, only root is supported now.' type: string required: - - Password - - User + - password + - user type: object param.CreateBackupPolicy: properties: @@ -50,32 +47,45 @@ definitions: bakDataPath: type: string bakEncryptionPassword: + example: encryptedPassword type: string destType: description: 'Enum: NFS, OSS' + example: NFS type: string - jobKeepWindow: - type: string + jobKeepDays: + example: 5 + type: integer ossAccessId: + example: encryptedPassword type: string ossAccessKey: + example: encryptedPassword type: string - pieceInterval: - type: string - recoveryWindow: - type: string + pieceIntervalDays: + example: 1 + type: integer + recoveryDays: + example: 3 + type: integer scheduleDates: items: $ref: '#/definitions/param.ScheduleDate' type: array scheduleTime: - description: 'Description: HH:MM' + description: |- + Description: HH:MM + Example: 04:00 + example: "04:00" type: string scheduleType: + description: 'Enum: Weekly, Monthly' + example: Weekly type: string required: + - archivePath + - bakDataPath - destType - - scheduleType type: object param.CreateNamespaceParam: properties: @@ -235,6 +245,7 @@ definitions: param.ReplayStandbyLog: properties: timestamp: + example: "2024-02-23 17:47:00" type: string unlimited: type: boolean @@ -268,10 +279,15 @@ definitions: type: string until: $ref: '#/definitions/param.RestoreUntilConfig' + required: + - archiveSource + - bakDataSource + - type type: object param.RestoreUntilConfig: properties: timestamp: + example: "2024-02-23 17:47:00" type: string unlimited: type: boolean @@ -285,8 +301,11 @@ definitions: properties: backupType: description: 'Enum: Full, Incremental' + example: Full type: string day: + description: 'Description: 1-31 for monthly, 1-7 for weekly' + example: 3 type: integer required: - backupType @@ -320,23 +339,31 @@ definitions: param.UpdateBackupPolicy: properties: jobKeepWindow: - type: string + example: 5 + type: integer pieceInterval: - type: string + example: 1 + type: integer recoveryWindow: - type: string + example: 3 + type: integer scheduleDates: items: $ref: '#/definitions/param.ScheduleDate' type: array + scheduleTime: + description: |- + Description: HH:MM + Example: 04:00 + example: "04:00" + type: string scheduleType: description: 'Enum: Weekly, Monthly' + example: Weekly type: string status: - description: 'Enum: Paused, Running' + description: 'Enum: PAUSED, RUNNING' type: string - required: - - status type: object param.UpgradeOBClusterParam: properties: @@ -401,35 +428,45 @@ definitions: type: string destType: description: 'Enum: NFS, OSS' + example: NFS type: string - jobKeepWindow: - type: string + jobKeepDays: + example: 5 + type: integer name: type: string namespace: type: string ossAccessSecret: type: string - pieceInterval: - type: string - recoveryWindow: - type: string + pieceIntervalDays: + example: 1 + type: integer + recoveryDays: + example: 3 + type: integer scheduleDates: items: $ref: '#/definitions/param.ScheduleDate' type: array scheduleTime: - description: 'Description: HH:MM' + description: |- + Description: HH:MM + Example: 04:00 + example: "04:00" type: string scheduleType: + description: 'Enum: Weekly, Monthly' + example: Weekly type: string status: type: string tenantName: type: string required: + - archivePath + - bakDataPath - destType - - scheduleType type: object response.DashboardInfo: properties: @@ -1641,16 +1678,6 @@ paths: encrypted by AES operationId: CreateTenant parameters: - - description: obtenant namespace - in: path - name: namespace - required: true - type: string - - description: obtenant name - in: path - name: name - required: true - type: string - description: create obtenant request body in: body name: body @@ -1973,7 +2000,7 @@ paths: summary: Get backup policy of specific tenant tags: - Obtenant - post: + patch: consumes: - application/json description: Update backup policy of specific tenant diff --git a/distribution/dashboard/internal/business/oceanbase/obtenant.go b/distribution/dashboard/internal/business/oceanbase/obtenant.go index b7cfa0a60..18df7ad27 100644 --- a/distribution/dashboard/internal/business/oceanbase/obtenant.go +++ b/distribution/dashboard/internal/business/oceanbase/obtenant.go @@ -20,7 +20,6 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/rand" - "k8s.io/client-go/util/retry" ) func buildOBTenantApiType(nn types.NamespacedName, p *param.CreateOBTenantParam) (*v1alpha1.OBTenant, error) { @@ -46,15 +45,27 @@ func buildOBTenantApiType(nn types.NamespacedName, p *param.CreateOBTenantParam) Source: &v1alpha1.TenantSourceSpec{}, }, } - if p.RootPassword != "" { - t.Spec.Credentials.Root = p.Name + "-root-" + rand.String(6) - } - t.Spec.Credentials.StandbyRO = p.Name + "-standbyro-" + rand.String(6) if len(p.Pools) == 0 { - return nil, errors.New("pools is empty") + return nil, oberr.NewBadRequest("pools is empty") + } + if p.UnitConfig == nil { + return nil, oberr.NewBadRequest("unit config is nil") } - // if len(p.Pools) > 0 { + + cpuCount, err := resource.ParseQuantity(p.UnitConfig.CPUCount) + if err != nil { + return nil, oberr.NewBadRequest("invalid cpu count: " + err.Error()) + } + memorySize, err := resource.ParseQuantity(p.UnitConfig.MemorySize) + if err != nil { + return nil, oberr.NewBadRequest("invalid memory size: " + err.Error()) + } + logDiskSize, err := resource.ParseQuantity(p.UnitConfig.LogDiskSize) + if err != nil { + return nil, oberr.NewBadRequest("invalid log disk size: " + err.Error()) + } + t.Spec.Pools = make([]v1alpha1.ResourcePoolSpec, 0, len(p.Pools)) for i := range p.Pools { apiPool := v1alpha1.ResourcePoolSpec{ @@ -68,19 +79,18 @@ func buildOBTenantApiType(nn types.NamespacedName, p *param.CreateOBTenantParam) Replica: 1, IsActive: true, } - if p.UnitConfig != nil { - apiPool.UnitConfig = &v1alpha1.UnitConfig{ - MaxCPU: resource.MustParse(p.UnitConfig.CPUCount), - MemorySize: resource.MustParse(p.UnitConfig.MemorySize), - MinCPU: resource.MustParse(p.UnitConfig.CPUCount), - LogDiskSize: resource.MustParse(p.UnitConfig.LogDiskSize), - MaxIops: p.UnitConfig.MaxIops, - MinIops: p.UnitConfig.MinIops, - IopsWeight: p.UnitConfig.IopsWeight, - } + apiPool.UnitConfig = &v1alpha1.UnitConfig{ + MaxCPU: cpuCount, + MemorySize: memorySize, + MinCPU: cpuCount, + LogDiskSize: logDiskSize, + MaxIops: p.UnitConfig.MaxIops, + MinIops: p.UnitConfig.MinIops, + IopsWeight: p.UnitConfig.IopsWeight, } t.Spec.Pools = append(t.Spec.Pools, apiPool) } + if p.Source != nil { t.Spec.Source = &v1alpha1.TenantSourceSpec{ Tenant: p.Source.Tenant, @@ -99,16 +109,6 @@ func buildOBTenantApiType(nn types.NamespacedName, p *param.CreateOBTenantParam) t.Spec.Source.Restore.BakDataSource.Type = apitypes.BackupDestType(p.Source.Restore.Type) t.Spec.Source.Restore.BakDataSource.Path = p.Source.Restore.BakDataSource - if p.Source.Restore.BakEncryptionPassword != "" { - t.Spec.Credentials.Root = p.Name + "-bak-encryption-" + rand.String(6) - } - - if p.Source.Restore.OSSAccessID != "" && p.Source.Restore.OSSAccessKey != "" { - ossName := p.Name + "-oss-access-" + rand.String(6) - t.Spec.Source.Restore.ArchiveSource.OSSAccessSecret = ossName - t.Spec.Source.Restore.BakDataSource.OSSAccessSecret = ossName - } - if p.Source.Restore.Until != nil { t.Spec.Source.Restore.Until.Timestamp = p.Source.Restore.Until.Timestamp } else { @@ -180,14 +180,38 @@ func buildBriefFromApiType(t *v1alpha1.OBTenant) *response.OBTenantBrief { return rt } +func updateOBTenant(ctx context.Context, nn types.NamespacedName, p *param.CreateOBTenantParam) (*response.OBTenantDetail, error) { + var err error + tenant, err := oceanbase.GetOBTenant(ctx, nn) + if err != nil { + return nil, err + } + t, err := buildOBTenantApiType(nn, p) + if err != nil { + return nil, err + } + + tenant.Spec = t.Spec + tenant, err = oceanbase.UpdateOBTenant(ctx, tenant) + if err != nil { + return nil, err + } + + return buildDetailFromApiType(tenant), nil +} + func CreateOBTenant(ctx context.Context, nn types.NamespacedName, p *param.CreateOBTenantParam) (*response.OBTenantDetail, error) { t, err := buildOBTenantApiType(nn, p) if err != nil { return nil, err } + if p.RootPassword != "" { + t.Spec.Credentials.Root = p.Name + "-root-" + rand.String(6) + } + + k8sclient := client.GetClient() if t.Spec.Credentials.Root != "" { - k8sclient := client.GetClient() - _, err = k8sclient.ClientSet.CoreV1().Secrets(nn.Namespace).Create(context.TODO(), &corev1.Secret{ + _, err = k8sclient.ClientSet.CoreV1().Secrets(nn.Namespace).Create(ctx, &corev1.Secret{ ObjectMeta: v1.ObjectMeta{ Name: t.Spec.Credentials.Root, Namespace: nn.Namespace, @@ -197,69 +221,62 @@ func CreateOBTenant(ctx context.Context, nn types.NamespacedName, p *param.Creat }, }, v1.CreateOptions{}) if err != nil { - return nil, err + return nil, oberr.NewInternal(err.Error()) } } - if t.Spec.Credentials.StandbyRO != "" { - k8sclient := client.GetClient() - _, err = k8sclient.ClientSet.CoreV1().Secrets(nn.Namespace).Create(context.TODO(), &corev1.Secret{ - ObjectMeta: v1.ObjectMeta{ - Name: t.Spec.Credentials.StandbyRO, - Namespace: nn.Namespace, - }, - StringData: map[string]string{ - "password": rand.String(20), - }, - }, v1.CreateOptions{}) - if err != nil { - return nil, err - } - } - if t.Spec.Source != nil && t.Spec.Source.Restore != nil && t.Spec.Source.Restore.BakEncryptionSecret != "" && - p.Source != nil && p.Source.Restore != nil && p.Source.Restore.BakEncryptionPassword != "" { - k8sclient := client.GetClient() - _, err = k8sclient.ClientSet.CoreV1().Secrets(nn.Namespace).Create(context.TODO(), &corev1.Secret{ - ObjectMeta: v1.ObjectMeta{ - Name: t.Spec.Credentials.Root, - Namespace: nn.Namespace, - }, - StringData: map[string]string{ - "password": p.Source.Restore.BakEncryptionPassword, - }, - }, v1.CreateOptions{}) - if err != nil { - return nil, err - } - } - tenant, err := oceanbase.CreateOBTenant(ctx, t) + t.Spec.Credentials.StandbyRO = p.Name + "-standbyro-" + rand.String(6) + _, err = k8sclient.ClientSet.CoreV1().Secrets(nn.Namespace).Create(ctx, &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: t.Spec.Credentials.StandbyRO, + Namespace: nn.Namespace, + }, + StringData: map[string]string{ + "password": rand.String(20), + }, + }, v1.CreateOptions{}) if err != nil { - return nil, err + return nil, oberr.NewInternal(err.Error()) } - return buildDetailFromApiType(tenant), nil -} -func updateOBTenant(ctx context.Context, nn types.NamespacedName, p *param.CreateOBTenantParam) (*response.OBTenantDetail, error) { - var err error - tenant, err := oceanbase.GetOBTenant(ctx, nn) - if err != nil { - return nil, err - } - t, err := buildOBTenantApiType(nn, p) - if err != nil { - return nil, err - } - err = retry.RetryOnConflict(retry.DefaultRetry, func() error { - tenant, err := oceanbase.GetOBTenant(ctx, nn) - if err != nil { - return err + if p.Source != nil && p.Source.Restore != nil { + if p.Source.Restore.BakEncryptionPassword != "" { + secretName := p.Name + "-bak-encryption-" + rand.String(6) + t.Spec.Source.Restore.BakEncryptionSecret = secretName + _, err = k8sclient.ClientSet.CoreV1().Secrets(nn.Namespace).Create(ctx, &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: secretName, + Namespace: nn.Namespace, + }, + StringData: map[string]string{ + "password": p.Source.Restore.BakEncryptionPassword, + }, + }, v1.CreateOptions{}) + if err != nil { + return nil, oberr.NewInternal(err.Error()) + } } - tenant.Spec = t.Spec - tenant, err = oceanbase.UpdateOBTenant(ctx, tenant) - if err != nil { - return err + + if p.Source.Restore.OSSAccessID != "" && p.Source.Restore.OSSAccessKey != "" { + ossSecretName := p.Name + "-oss-access-" + rand.String(6) + t.Spec.Source.Restore.ArchiveSource.OSSAccessSecret = ossSecretName + t.Spec.Source.Restore.BakDataSource.OSSAccessSecret = ossSecretName + _, err = k8sclient.ClientSet.CoreV1().Secrets(nn.Namespace).Create(ctx, &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: ossSecretName, + Namespace: nn.Namespace, + }, + StringData: map[string]string{ + "accessId": p.Source.Restore.OSSAccessID, + "accessKey": p.Source.Restore.OSSAccessKey, + }, + }, v1.CreateOptions{}) + if err != nil { + return nil, oberr.NewInternal(err.Error()) + } } - return nil - }) + } + + tenant, err := oceanbase.CreateOBTenant(ctx, t) if err != nil { return nil, err } @@ -290,48 +307,6 @@ func DeleteOBTenant(ctx context.Context, nn types.NamespacedName) error { return oceanbase.DeleteOBTenant(ctx, nn) } -func ModifyOBTenantUnitNumber(ctx context.Context, nn types.NamespacedName, unitNumber int) (*response.OBTenantDetail, error) { - var err error - tenant, err := oceanbase.GetOBTenant(ctx, nn) - if err != nil { - return nil, err - } - - tenant.Spec.UnitNumber = unitNumber - tenant, err = oceanbase.UpdateOBTenant(ctx, tenant) - if err != nil { - return nil, err - } - return buildDetailFromApiType(tenant), nil -} - -func ModifyOBTenantUnitConfig(ctx context.Context, nn types.NamespacedName, zone string, unitConfig *param.UnitConfig) (*response.OBTenantDetail, error) { - var err error - tenant, err := oceanbase.GetOBTenant(ctx, nn) - if err != nil { - return nil, err - } - for i := range tenant.Spec.Pools { - if tenant.Spec.Pools[i].Zone == zone { - tenant.Spec.Pools[i].UnitConfig = &v1alpha1.UnitConfig{ - MaxCPU: resource.MustParse(unitConfig.CPUCount), - MemorySize: resource.MustParse(unitConfig.MemorySize), - MinCPU: resource.MustParse(unitConfig.CPUCount), - LogDiskSize: resource.MustParse(unitConfig.LogDiskSize), - MaxIops: unitConfig.MaxIops, - MinIops: unitConfig.MinIops, - IopsWeight: unitConfig.IopsWeight, - } - break - } - } - tenant, err = oceanbase.UpdateOBTenant(ctx, tenant) - if err != nil { - return nil, err - } - return buildDetailFromApiType(tenant), nil -} - func ModifyOBTenantRootPassword(ctx context.Context, nn types.NamespacedName, rootPassword string) (*response.OBTenantDetail, error) { var err error tenant, err := oceanbase.GetOBTenant(ctx, nn) @@ -341,7 +316,7 @@ func ModifyOBTenantRootPassword(ctx context.Context, nn types.NamespacedName, ro // create new secret k8sclient := client.GetClient() newRootSecretName := nn.Name + "-root-" + rand.String(6) - _, err = k8sclient.ClientSet.CoreV1().Secrets(nn.Namespace).Create(context.TODO(), &corev1.Secret{ + _, err = k8sclient.ClientSet.CoreV1().Secrets(nn.Namespace).Create(ctx, &corev1.Secret{ ObjectMeta: v1.ObjectMeta{ Name: newRootSecretName, Namespace: nn.Namespace, @@ -353,8 +328,8 @@ func ModifyOBTenantRootPassword(ctx context.Context, nn types.NamespacedName, ro changePwdOp := v1alpha1.OBTenantOperation{ ObjectMeta: v1.ObjectMeta{ - GenerateName: nn.Name + "-change-root-pwd-", - Namespace: nn.Namespace, + Name: nn.Name + "-change-root-pwd-" + rand.String(6), + Namespace: nn.Namespace, }, Spec: v1alpha1.OBTenantOperationSpec{ Type: apiconst.TenantOpChangePwd, @@ -371,7 +346,7 @@ func ModifyOBTenantRootPassword(ctx context.Context, nn types.NamespacedName, ro return buildDetailFromApiType(tenant), nil } -func ReplayStandbyLog(ctx context.Context, nn types.NamespacedName, timestamp string) (*response.OBTenantDetail, error) { +func ReplayStandbyLog(ctx context.Context, nn types.NamespacedName, param *param.ReplayStandbyLog) (*response.OBTenantDetail, error) { var err error tenant, err := oceanbase.GetOBTenant(ctx, nn) if err != nil { @@ -382,13 +357,14 @@ func ReplayStandbyLog(ctx context.Context, nn types.NamespacedName, timestamp st } replayLogOp := v1alpha1.OBTenantOperation{ ObjectMeta: v1.ObjectMeta{ - GenerateName: nn.Name + "-replay-log-", - Namespace: nn.Namespace, + Name: nn.Name + "-replay-log-" + rand.String(6), + Namespace: nn.Namespace, }, Spec: v1alpha1.OBTenantOperationSpec{ Type: apiconst.TenantOpReplayLog, ReplayUntil: &v1alpha1.RestoreUntilConfig{ - Timestamp: ×tamp, + Timestamp: param.Timestamp, + Unlimited: param.Unlimited, }, TargetTenant: &nn.Name, }, @@ -411,8 +387,8 @@ func UpgradeTenantVersion(ctx context.Context, nn types.NamespacedName) (*respon } upgradeOp := v1alpha1.OBTenantOperation{ ObjectMeta: v1.ObjectMeta{ - GenerateName: nn.Name + "-upgrade-", - Namespace: nn.Namespace, + Name: nn.Name + "-upgrade-" + rand.String(6), + Namespace: nn.Namespace, }, Spec: v1alpha1.OBTenantOperationSpec{ Type: apiconst.TenantOpUpgrade, @@ -432,26 +408,30 @@ func ChangeTenantRole(ctx context.Context, nn types.NamespacedName, p *param.Cha if err != nil { return nil, err } - if tenant.Status.TenantRole == apitypes.TenantRole(p.TenantRole) { - return nil, oberr.NewBadRequest("The tenant is already " + string(p.TenantRole)) + if tenant.Status.TenantRole == apiconst.TenantRolePrimary && p.Failover { + return nil, oberr.NewBadRequest("The tenant is already PRIMARY") } if p.Switchover && (tenant.Status.Source == nil || tenant.Status.Source.Tenant == nil) { return nil, oberr.NewBadRequest("The tenant has no primary tenant") } changeRoleOp := v1alpha1.OBTenantOperation{ ObjectMeta: v1.ObjectMeta{ - GenerateName: nn.Name + "-change-role-", - Namespace: nn.Namespace, + Name: nn.Name + "-change-role-" + rand.String(6), + Namespace: nn.Namespace, }, Spec: v1alpha1.OBTenantOperationSpec{}, } if p.Switchover { changeRoleOp.Spec.Type = apiconst.TenantOpSwitchover - changeRoleOp.Spec.Switchover.PrimaryTenant = *tenant.Status.Source.Tenant - changeRoleOp.Spec.Switchover.StandbyTenant = nn.Name - } else { + changeRoleOp.Spec.Switchover = &v1alpha1.OBTenantOpSwitchoverSpec{ + PrimaryTenant: *tenant.Status.Source.Tenant, + StandbyTenant: nn.Name, + } + } else if p.Failover { changeRoleOp.Spec.Type = apiconst.TenantOpFailover - changeRoleOp.Spec.Failover.StandbyTenant = nn.Name + changeRoleOp.Spec.Failover = &v1alpha1.OBTenantOpFailoverSpec{ + StandbyTenant: nn.Name, + } } _, err = oceanbase.CreateOBTenantOperation(ctx, &changeRoleOp) if err != nil { @@ -470,19 +450,31 @@ func PatchTenant(ctx context.Context, nn types.NamespacedName, p *param.PatchTen tenant.Spec.UnitNumber = *p.UnitNumber } if p.UnitConfig != nil { + cpuCount, err := resource.ParseQuantity(p.UnitConfig.UnitConfig.CPUCount) + if err != nil { + return nil, oberr.NewBadRequest("invalid cpu count: " + err.Error()) + } + memorySize, err := resource.ParseQuantity(p.UnitConfig.UnitConfig.MemorySize) + if err != nil { + return nil, oberr.NewBadRequest("invalid memory size: " + err.Error()) + } + logDiskSize, err := resource.ParseQuantity(p.UnitConfig.UnitConfig.LogDiskSize) + if err != nil { + return nil, oberr.NewBadRequest("invalid log disk size: " + err.Error()) + } for _, pool := range p.UnitConfig.Pools { for i := range tenant.Spec.Pools { if tenant.Spec.Pools[i].Zone == pool.Zone { tenant.Spec.Pools[i].Priority = pool.Priority tenant.Spec.Pools[i].Type.Name = pool.Type tenant.Spec.Pools[i].UnitConfig = &v1alpha1.UnitConfig{ - MaxCPU: resource.MustParse(p.UnitConfig.UnitConfig.CPUCount), - MemorySize: resource.MustParse(p.UnitConfig.UnitConfig.MemorySize), - MinCPU: resource.MustParse(p.UnitConfig.UnitConfig.CPUCount), + MaxCPU: cpuCount, + MemorySize: memorySize, + MinCPU: cpuCount, IopsWeight: p.UnitConfig.UnitConfig.IopsWeight, MaxIops: p.UnitConfig.UnitConfig.MaxIops, MinIops: p.UnitConfig.UnitConfig.MinIops, - LogDiskSize: resource.MustParse(p.UnitConfig.UnitConfig.LogDiskSize), + LogDiskSize: logDiskSize, } break } diff --git a/distribution/dashboard/internal/business/oceanbase/obtenantbackup.go b/distribution/dashboard/internal/business/oceanbase/obtenantbackup.go index 8938b8470..dccd39ac7 100644 --- a/distribution/dashboard/internal/business/oceanbase/obtenantbackup.go +++ b/distribution/dashboard/internal/business/oceanbase/obtenantbackup.go @@ -13,55 +13,33 @@ import ( "github.com/oceanbase/oceanbase-dashboard/internal/model/param" "github.com/oceanbase/oceanbase-dashboard/internal/model/response" oberr "github.com/oceanbase/oceanbase-dashboard/pkg/errors" + "github.com/oceanbase/oceanbase-dashboard/pkg/k8s/client" "github.com/oceanbase/oceanbase-dashboard/pkg/oceanbase" + corev1 "k8s.io/api/core/v1" kubeerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/rand" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/rand" ) -func buildBackupPolicyApiType(nn types.NamespacedName, obcluster string, p *param.CreateBackupPolicy) *v1alpha1.OBTenantBackupPolicy { - policy := &v1alpha1.OBTenantBackupPolicy{} - policy.Name = nn.Name + "-backup-policy" - policy.Namespace = nn.Namespace - policy.Spec = v1alpha1.OBTenantBackupPolicySpec{ - ObClusterName: obcluster, - TenantCRName: nn.Name, - JobKeepWindow: p.JobKeepWindow, - LogArchive: v1alpha1.LogArchiveConfig{ - Destination: apitypes.BackupDestination{ - Path: p.ArchivePath, - Type: apitypes.BackupDestType(p.DestType), - OSSAccessSecret: "", - }, - SwitchPieceInterval: "1d", - }, - DataBackup: v1alpha1.DataBackupConfig{ - Destination: apitypes.BackupDestination{ - Path: p.BakDataPath, - Type: apitypes.BackupDestType(p.DestType), - OSSAccessSecret: "", - }, - FullCrontab: "", - IncrementalCrontab: "", - EncryptionSecret: "", - }, - DataClean: v1alpha1.CleanPolicy{ - RecoveryWindow: p.RecoveryWindow, - }, - } - if p.DestType == "OSS" && p.OSSAccessID != "" && p.OSSAccessKey != "" { - ossSecretName := nn.Name + "-backup-oss-secret-" + rand.String(6) - policy.Spec.LogArchive.Destination.OSSAccessSecret = ossSecretName - policy.Spec.DataBackup.Destination.OSSAccessSecret = ossSecretName +func numberToDay(n int) string { + return fmt.Sprintf("%dd", n) +} + +func dayToNumber(day string) int { + if !strings.HasSuffix(day, "d") { + return 0 } - if p.BakEncryptionPassword != "" { - encryptionSecretName := nn.Name + "-backup-encryption-secret-" + rand.String(6) - policy.Spec.DataBackup.EncryptionSecret = encryptionSecretName + n, err := strconv.Atoi(day[:len(day)-1]) + if err != nil { + return 0 } + return n +} +func setScheduleDatesToPolicy(policy *v1alpha1.OBTenantBackupPolicy, p param.ScheduleBase) { hourMinutes := strings.Split(p.ScheduleTime, ":") crontabParts := fmt.Sprintf("%s %s", hourMinutes[1], hourMinutes[0]) @@ -77,7 +55,11 @@ func buildBackupPolicyApiType(nn types.NamespacedName, obcluster string, p *para } } policy.Spec.DataBackup.FullCrontab = crontabParts + " " + strings.Join(fullCrontabWeekdays, ",") - policy.Spec.DataBackup.IncrementalCrontab = crontabParts + " " + strings.Join(incrementalCrontabWeekdays, ",") + if len(incrementalCrontabWeekdays) > 0 { + policy.Spec.DataBackup.IncrementalCrontab = crontabParts + " " + strings.Join(incrementalCrontabWeekdays, ",") + } else { + policy.Spec.DataBackup.IncrementalCrontab = crontabParts + " *" + } } else if p.ScheduleType == "Monthly" { fullCrontabMonthdays := make([]string, 0) incrementalCrontabMonthdays := make([]string, 0) @@ -89,32 +71,16 @@ func buildBackupPolicyApiType(nn types.NamespacedName, obcluster string, p *para } } policy.Spec.DataBackup.FullCrontab = strings.Join([]string{crontabParts, strings.Join(fullCrontabMonthdays, ","), "* *"}, " ") - policy.Spec.DataBackup.IncrementalCrontab = strings.Join([]string{crontabParts, strings.Join(incrementalCrontabMonthdays, ","), "* *"}, " ") + if len(incrementalCrontabMonthdays) > 0 { + policy.Spec.DataBackup.IncrementalCrontab = strings.Join([]string{crontabParts, strings.Join(incrementalCrontabMonthdays, ","), "* *"}, " ") + } else { + policy.Spec.DataBackup.IncrementalCrontab = strings.Join([]string{crontabParts, "*", "* *"}, " ") + } } - return policy } -func buildBackupPolicyModelType(p *v1alpha1.OBTenantBackupPolicy) *response.BackupPolicy { - res := &response.BackupPolicy{ - BackupPolicyBase: param.BackupPolicyBase{ - DestType: param.BackupDestType(p.Spec.DataBackup.Destination.Type), - ArchivePath: p.Spec.LogArchive.Destination.Path, - BakDataPath: p.Spec.DataBackup.Destination.Path, - ScheduleType: "", - ScheduleTime: "", - ScheduleDates: []param.ScheduleDate{}, - JobKeepWindow: p.Spec.JobKeepWindow, - RecoveryWindow: p.Spec.DataClean.RecoveryWindow, - PieceInterval: p.Spec.LogArchive.SwitchPieceInterval, - }, - TenantName: p.Spec.TenantCRName, - Name: p.Name, - Namespace: p.Namespace, - Status: string(p.Status.Status), - OSSAccessSecret: p.Spec.LogArchive.Destination.OSSAccessSecret, - BakEncryptionSecret: p.Spec.DataBackup.EncryptionSecret, - } - +func getScheduleDatesFromPolicy(p *v1alpha1.OBTenantBackupPolicy) param.ScheduleBase { + res := param.ScheduleBase{} fullParts := strings.Split(p.Spec.DataBackup.FullCrontab, " ") incrementalParts := strings.Split(p.Spec.DataBackup.IncrementalCrontab, " ") res.ScheduleTime = fmt.Sprintf("%s:%s", fullParts[1], fullParts[0]) @@ -144,6 +110,10 @@ func buildBackupPolicyModelType(p *v1alpha1.OBTenantBackupPolicy) *response.Back var i, j int for i < len(fullDays) && j < len(incrementalDays) { fullDay, _ := strconv.Atoi(fullDays[i]) + if incrementalDays[j] == "*" { + j = len(incrementalDays) + break + } incrementalDay, _ := strconv.Atoi(incrementalDays[j]) // It should not happen, but just in case if fullDay == incrementalDay { @@ -186,6 +156,69 @@ func buildBackupPolicyModelType(p *v1alpha1.OBTenantBackupPolicy) *response.Back return res } +func buildBackupPolicyApiType(nn types.NamespacedName, obcluster string, p *param.CreateBackupPolicy) *v1alpha1.OBTenantBackupPolicy { + policy := &v1alpha1.OBTenantBackupPolicy{} + policy.Name = nn.Name + "-backup-policy" + policy.Namespace = nn.Namespace + policy.Spec = v1alpha1.OBTenantBackupPolicySpec{ + ObClusterName: obcluster, + TenantCRName: nn.Name, + TenantName: nn.Name, // It's tricky to use the deprecated field + JobKeepWindow: numberToDay(p.JobKeepDays), + LogArchive: v1alpha1.LogArchiveConfig{ + Destination: apitypes.BackupDestination{ + Path: p.ArchivePath, + Type: apitypes.BackupDestType(p.DestType), + OSSAccessSecret: "", + }, + SwitchPieceInterval: "1d", + }, + DataBackup: v1alpha1.DataBackupConfig{ + Destination: apitypes.BackupDestination{ + Path: p.BakDataPath, + Type: apitypes.BackupDestType(p.DestType), + OSSAccessSecret: "", + }, + FullCrontab: "", + IncrementalCrontab: "", + EncryptionSecret: "", + }, + DataClean: v1alpha1.CleanPolicy{ + RecoveryWindow: numberToDay(p.RecoveryDays), + }, + } + + setScheduleDatesToPolicy(policy, p.ScheduleBase) + + return policy +} + +func buildBackupPolicyModelType(p *v1alpha1.OBTenantBackupPolicy) *response.BackupPolicy { + res := &response.BackupPolicy{ + BackupPolicyBase: param.BackupPolicyBase{ + DestType: param.BackupDestType(p.Spec.DataBackup.Destination.Type), + ArchivePath: p.Spec.LogArchive.Destination.Path, + BakDataPath: p.Spec.DataBackup.Destination.Path, + ScheduleBase: param.ScheduleBase{ + ScheduleType: "", + ScheduleTime: "", + ScheduleDates: []param.ScheduleDate{}, + }, + JobKeepDays: dayToNumber(p.Spec.JobKeepWindow), + RecoveryDays: dayToNumber(p.Spec.DataClean.RecoveryWindow), + PieceIntervalDays: dayToNumber(p.Spec.LogArchive.SwitchPieceInterval), + }, + TenantName: p.Spec.TenantCRName, + Name: p.Name, + Namespace: p.Namespace, + Status: string(p.Status.Status), + OSSAccessSecret: p.Spec.LogArchive.Destination.OSSAccessSecret, + BakEncryptionSecret: p.Spec.DataBackup.EncryptionSecret, + } + res.ScheduleBase = getScheduleDatesFromPolicy(p) + return res +} + func buildBackupJobModelType(p *v1alpha1.OBTenantBackup) *response.BackupJob { if p == nil { return nil @@ -210,11 +243,17 @@ func buildBackupJobModelType(p *v1alpha1.OBTenantBackup) *response.BackupJob { } switch p.Spec.Type { case apiconst.BackupJobTypeFull, apiconst.BackupJobTypeIncr: - res.StatusInDatabase = p.Status.BackupJob.Status + if p.Status.BackupJob != nil { + res.StatusInDatabase = p.Status.BackupJob.Status + } case apiconst.BackupJobTypeArchive: - res.StatusInDatabase = p.Status.ArchiveLogJob.Status + if p.Status.ArchiveLogJob != nil { + res.StatusInDatabase = p.Status.ArchiveLogJob.Status + } case apiconst.BackupJobTypeClean: - res.StatusInDatabase = p.Status.DataCleanJob.Status + if p.Status.DataCleanJob != nil { + res.StatusInDatabase = p.Status.DataCleanJob.Status + } } return res } @@ -249,6 +288,44 @@ func CreateTenantBackupPolicy(ctx context.Context, nn types.NamespacedName, p *p return nil, oberr.NewBadRequest("Tenant is not running") } backupPolicy := buildBackupPolicyApiType(nn, tenant.Spec.ClusterName, p) + + if p.DestType == "OSS" && p.OSSAccessID != "" && p.OSSAccessKey != "" { + ossSecretName := nn.Name + "-backup-oss-secret-" + rand.String(6) + backupPolicy.Spec.LogArchive.Destination.OSSAccessSecret = ossSecretName + backupPolicy.Spec.DataBackup.Destination.OSSAccessSecret = ossSecretName + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: ossSecretName, + Namespace: nn.Namespace, + }, + StringData: map[string]string{ + "accessId": p.OSSAccessID, + "accessKey": p.OSSAccessKey, + }, + } + _, err := client.GetClient().ClientSet.CoreV1().Secrets(nn.Namespace).Create(ctx, secret, metav1.CreateOptions{}) + if err != nil { + return nil, oberr.NewInternal(err.Error()) + } + } + if p.BakEncryptionPassword != "" { + encryptionSecretName := nn.Name + "-backup-encryption-secret-" + rand.String(6) + backupPolicy.Spec.DataBackup.EncryptionSecret = encryptionSecretName + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: encryptionSecretName, + Namespace: nn.Namespace, + }, + StringData: map[string]string{ + "password": p.BakEncryptionPassword, + }, + } + _, err := client.GetClient().ClientSet.CoreV1().Secrets(nn.Namespace).Create(ctx, secret, metav1.CreateOptions{}) + if err != nil { + return nil, oberr.NewInternal(err.Error()) + } + } + policy, err := oceanbase.CreateTenantBackupPolicy(ctx, backupPolicy) if err != nil { return nil, oberr.NewInternal(err.Error()) @@ -271,15 +348,37 @@ func UpdateTenantBackupPolicy(ctx context.Context, nn types.NamespacedName, p *p if err != nil { return nil, oberr.NewBadRequest(err.Error()) } - policy.Spec.JobKeepWindow = p.JobKeepWindow - policy.Spec.DataClean.RecoveryWindow = p.RecoveryWindow - policy.Spec.LogArchive.SwitchPieceInterval = p.PieceInterval - if p.Status == "Paused" { - policy.Spec.Suspend = true + if p.JobKeepWindow != 0 { + policy.Spec.JobKeepWindow = numberToDay(p.JobKeepWindow) + } + if p.RecoveryWindow != 0 { + policy.Spec.DataClean.RecoveryWindow = numberToDay(p.RecoveryWindow) } - if p.Status == "Running" { + if p.PieceInterval != 0 { + policy.Spec.LogArchive.SwitchPieceInterval = numberToDay(p.PieceInterval) + } + + if strings.ToUpper(p.Status) == "PAUSED" { + policy.Spec.Suspend = true + } else if strings.ToUpper(p.Status) == "RUNNING" { policy.Spec.Suspend = false } + + schedule := p.ScheduleBase + if schedule.ScheduleDates != nil || schedule.ScheduleTime != "" || schedule.ScheduleType != "" { + overlaySchedule := getScheduleDatesFromPolicy(policy) + if schedule.ScheduleType != "" { + overlaySchedule.ScheduleType = schedule.ScheduleType + } + if schedule.ScheduleTime != "" { + overlaySchedule.ScheduleTime = schedule.ScheduleTime + } + if schedule.ScheduleDates != nil { + overlaySchedule.ScheduleDates = schedule.ScheduleDates + } + setScheduleDatesToPolicy(policy, overlaySchedule) + } + np, err := oceanbase.UpdateTenantBackupPolicy(ctx, policy) if err != nil { return nil, oberr.NewInternal(err.Error()) @@ -300,6 +399,9 @@ func ListBackupJobs(ctx context.Context, nn types.NamespacedName, jobType string if err != nil { return nil, oberr.NewInternal(err.Error()) } + if policy == nil { + return nil, nil + } listOption := metav1.ListOptions{} if jobType != "" && jobType != "ALL" { listOption.LabelSelector = oceanbaseconst.LabelRefBackupPolicy + "=" + policy.Name + "," + oceanbaseconst.LabelBackupType + "=" + jobType diff --git a/distribution/dashboard/internal/business/oceanbase/obtenantbackup_test.go b/distribution/dashboard/internal/business/oceanbase/obtenantbackup_test.go index 6d598c797..d665685e3 100644 --- a/distribution/dashboard/internal/business/oceanbase/obtenantbackup_test.go +++ b/distribution/dashboard/internal/business/oceanbase/obtenantbackup_test.go @@ -27,14 +27,16 @@ func TestCreateOBTenantBackupPolicyWeekly(t *testing.T) { }} p := param.CreateBackupPolicy{ BackupPolicyBase: param.BackupPolicyBase{ - DestType: "NFS", - ArchivePath: "archive/t1", - BakDataPath: "backup/t1", - ScheduleType: "Weekly", - ScheduleDates: scheduleDates, - ScheduleTime: "04:00", - JobKeepWindow: "3d", - RecoveryWindow: "7d", + DestType: "NFS", + ArchivePath: "archive/t1", + BakDataPath: "backup/t1", + ScheduleBase: param.ScheduleBase{ + ScheduleType: "Weekly", + ScheduleDates: scheduleDates, + ScheduleTime: "04:00", + }, + JobKeepDays: 3, + RecoveryDays: 7, }, } policy := buildBackupPolicyApiType(types.NamespacedName{Name: "t1", Namespace: "default"}, "fake-cluster", &p) @@ -85,14 +87,16 @@ func TestCreateOBTenantBackupPolicyMonthly(t *testing.T) { }} p := param.CreateBackupPolicy{ BackupPolicyBase: param.BackupPolicyBase{ - DestType: "NFS", - ArchivePath: "archive/t1", - BakDataPath: "backup/t1", - ScheduleType: "Monthly", - ScheduleDates: scheduleDates, - ScheduleTime: "04:00", - JobKeepWindow: "3d", - RecoveryWindow: "7d", + DestType: "NFS", + ArchivePath: "archive/t1", + BakDataPath: "backup/t1", + ScheduleBase: param.ScheduleBase{ + ScheduleType: "Monthly", + ScheduleDates: scheduleDates, + ScheduleTime: "04:00", + }, + JobKeepDays: 3, + RecoveryDays: 7, }, } policy := buildBackupPolicyApiType(types.NamespacedName{Name: "t1", Namespace: "default"}, "fake-cluster", &p) diff --git a/distribution/dashboard/internal/handler/obtenant_handler.go b/distribution/dashboard/internal/handler/obtenant_handler.go index 5b303c802..bae5da206 100644 --- a/distribution/dashboard/internal/handler/obtenant_handler.go +++ b/distribution/dashboard/internal/handler/obtenant_handler.go @@ -3,6 +3,7 @@ package handler import ( "fmt" "strconv" + "strings" "github.com/gin-gonic/gin" "github.com/oceanbase/oceanbase-dashboard/internal/business/oceanbase" @@ -42,6 +43,17 @@ func ListAllTenants(c *gin.Context) ([]*response.OBTenantBrief, error) { if err != nil { return nil, httpErr.NewInternal(err.Error()) } + if len(tenants) == 0 && c.Query("obcluster") != "" { + allTenants, err := oceanbase.ListAllOBTenants(c, metav1.ListOptions{}) + if err != nil { + return nil, httpErr.NewInternal(err.Error()) + } + for i := range allTenants { + if allTenants[i].ClusterName == c.Query("obcluster") { + tenants = append(tenants, allTenants[i]) + } + } + } return tenants, nil } @@ -84,8 +96,6 @@ func GetTenant(c *gin.Context) (*response.OBTenantDetail, error) { // @Description Create an obtenant in a specific namespace, passwords should be encrypted by AES // @Accept application/json // @Produce application/json -// @Param namespace path string true "obtenant namespace" -// @Param name path string true "obtenant name" // @Param body body param.CreateOBTenantParam true "create obtenant request body" // @Success 200 object response.APIResponse{data=response.OBTenantDetail} // @Failure 400 object response.APIResponse @@ -101,8 +111,8 @@ func CreateTenant(c *gin.Context) (*response.OBTenantDetail, error) { } logger.Infof("Create obtenant: %+v", tenantParam) tenant, err := oceanbase.CreateOBTenant(c, types.NamespacedName{ - Namespace: tenantParam.Name, - Name: tenantParam.Namespace, + Namespace: tenantParam.Namespace, + Name: tenantParam.Name, }, tenantParam) if err != nil { return nil, httpErr.NewInternal(err.Error()) @@ -143,54 +153,6 @@ func DeleteTenant(c *gin.Context) (interface{}, error) { return nil, nil } -// @Deprecated: use PatchTenant instead -func ModifyUnitNumber(c *gin.Context) (*response.OBTenantDetail, error) { - nn := ¶m.NamespacedName{} - err := c.BindUri(nn) - if err != nil { - return nil, httpErr.NewBadRequest(err.Error()) - } - unitNumberParam := ¶m.ModifyUnitNumber{} - err = c.BindJSON(unitNumberParam) - if err != nil { - return nil, httpErr.NewBadRequest(err.Error()) - } - tenant, err := oceanbase.ModifyOBTenantUnitNumber(c, types.NamespacedName{ - Namespace: nn.Namespace, - Name: nn.Name, - }, unitNumberParam.UnitNumber) - if err != nil { - return nil, httpErr.NewInternal(err.Error()) - } - return tenant, nil -} - -// @Deprecated: use PatchTenant instead -func ModifyUnitConfig(c *gin.Context) (*response.OBTenantDetail, error) { - nn := struct { - Name string `uri:"name"` - Namespace string `uri:"namespace"` - Zone string `uri:"zone"` - }{} - err := c.BindUri(&nn) - if err != nil { - return nil, httpErr.NewBadRequest(err.Error()) - } - unitConfig := param.UnitConfig{} - err = c.BindJSON(&unitConfig) - if err != nil { - return nil, httpErr.NewBadRequest(err.Error()) - } - tenant, err := oceanbase.ModifyOBTenantUnitConfig(c, types.NamespacedName{ - Namespace: nn.Namespace, - Name: nn.Name, - }, nn.Zone, &unitConfig) - if err != nil { - return nil, httpErr.NewInternal(err.Error()) - } - return tenant, nil -} - // @ID PatchTenant // @Tags Obtenant // @Summary Patch tenant's configuration @@ -296,13 +258,13 @@ func ReplayStandbyLog(c *gin.Context) (*response.OBTenantDetail, error) { if err != nil { return nil, httpErr.NewBadRequest(err.Error()) } - if logReplayParam.Timestamp == nil { - return nil, httpErr.NewBadRequest("timestamp is required") + if !logReplayParam.Unlimited && logReplayParam.Timestamp == nil { + return nil, httpErr.NewBadRequest("timestamp is required if the restore is limited") } tenant, err := oceanbase.ReplayStandbyLog(c, types.NamespacedName{ Name: nn.Name, Namespace: nn.Namespace, - }, *logReplayParam.Timestamp) + }, logReplayParam) if err != nil { return nil, httpErr.NewInternal(err.Error()) } @@ -365,6 +327,9 @@ func ChangeTenantRole(c *gin.Context) (*response.OBTenantDetail, error) { if err != nil { return nil, httpErr.NewBadRequest(err.Error()) } + if !p.Failover != p.Switchover { + return nil, httpErr.NewBadRequest("one and only one of failover and switchover can be true") + } tenant, err := oceanbase.ChangeTenantRole(c, types.NamespacedName{ Name: nn.Name, Namespace: nn.Namespace, @@ -424,7 +389,7 @@ func CreateBackupPolicy(c *gin.Context) (*response.BackupPolicy, error) { // @Param namespace path string true "obtenant namespace" // @Param name path string true "obtenant name" // @Param body body param.UpdateBackupPolicy true "update backup policy request body" -// @Router /api/v1/obtenants/{namespace}/{name}/backupPolicy [POST] +// @Router /api/v1/obtenants/{namespace}/{name}/backupPolicy [PATCH] // @Security ApiKeyAuth func UpdateBackupPolicy(c *gin.Context) (*response.BackupPolicy, error) { nn := ¶m.NamespacedName{} @@ -533,6 +498,7 @@ func ListBackupJobs(c *gin.Context) ([]*response.BackupJob, error) { if err != nil { return nil, httpErr.NewBadRequest(err.Error()) } + p.Type = strings.ToUpper(p.Type) limit := 10 if c.Query("limit") != "" { limit, err = strconv.Atoi(c.Query("limit")) diff --git a/distribution/dashboard/internal/handler/wrapper.go b/distribution/dashboard/internal/handler/wrapper.go index 0a8a6847b..f6087c1cd 100644 --- a/distribution/dashboard/internal/handler/wrapper.go +++ b/distribution/dashboard/internal/handler/wrapper.go @@ -27,7 +27,7 @@ func Wrap[T any](h Handler[T]) gin.HandlerFunc { statusCode := http.StatusOK var errMsg string if err != nil { - if obe := err.(errors.ObError); obe != nil { + if obe, ok := err.(errors.ObError); ok && obe != nil { statusCode = obe.Status() } else { statusCode = http.StatusInternalServerError diff --git a/distribution/dashboard/internal/model/param/backup_param.go b/distribution/dashboard/internal/model/param/backup_param.go index 4d79a25df..b820e263a 100644 --- a/distribution/dashboard/internal/model/param/backup_param.go +++ b/distribution/dashboard/internal/model/param/backup_param.go @@ -1,43 +1,48 @@ package param +type ScheduleBase struct { + // Enum: Weekly, Monthly + ScheduleType string `json:"scheduleType" example:"Weekly"` + ScheduleDates []ScheduleDate `json:"scheduleDates"` + // Description: HH:MM + // Example: 04:00 + ScheduleTime string `json:"scheduleTime" example:"04:00"` +} + type BackupPolicyBase struct { // Enum: NFS, OSS - DestType BackupDestType `json:"destType" binding:"required"` - ArchivePath string `json:"archivePath"` - BakDataPath string `json:"bakDataPath"` + DestType BackupDestType `json:"destType" binding:"required" example:"NFS"` + ArchivePath string `json:"archivePath" binding:"required"` + BakDataPath string `json:"bakDataPath" binding:"required"` - ScheduleType string `json:"scheduleType" binding:"required"` - ScheduleDates []ScheduleDate `json:"scheduleDates"` - // Description: HH:MM - ScheduleTime string `json:"scheduleTime,omitempty"` + ScheduleBase `json:",inline"` - JobKeepWindow string `json:"jobKeepWindow,omitempty"` - RecoveryWindow string `json:"recoveryWindow,omitempty"` - PieceInterval string `json:"pieceInterval,omitempty"` + JobKeepDays int `json:"jobKeepDays,omitempty" example:"5"` + RecoveryDays int `json:"recoveryDays,omitempty" example:"3"` + PieceIntervalDays int `json:"pieceIntervalDays,omitempty" example:"1"` } type CreateBackupPolicy struct { BackupPolicyBase `json:",inline"` - OSSAccessID string `json:"ossAccessId,omitempty"` - OSSAccessKey string `json:"ossAccessKey,omitempty"` - BakEncryptionPassword string `json:"bakEncryptionPassword,omitempty"` + OSSAccessID string `json:"ossAccessId,omitempty" example:"encryptedPassword"` + OSSAccessKey string `json:"ossAccessKey,omitempty" example:"encryptedPassword"` + BakEncryptionPassword string `json:"bakEncryptionPassword,omitempty" example:"encryptedPassword"` } type ScheduleDate struct { - Day int `json:"day" binding:"required"` + // Description: 1-31 for monthly, 1-7 for weekly + Day int `json:"day" binding:"required" example:"3"` // Enum: Full, Incremental - BackupType string `json:"backupType" binding:"required"` + BackupType string `json:"backupType" binding:"required" example:"Full"` } type UpdateBackupPolicy struct { - // Enum: Paused, Running - Status string `json:"status" binding:"required"` + // Enum: PAUSED, RUNNING + Status string `json:"status,omitempty" exmaple:"PAUSED"` - // Enum: Weekly, Monthly - ScheduleType string `json:"scheduleType,omitempty"` - ScheduleDates []ScheduleDate `json:"scheduleDates,omitempty"` + ScheduleBase `json:",inline,omitempty"` - JobKeepWindow string `json:"jobKeepWindow,omitempty"` - RecoveryWindow string `json:"recoveryWindow,omitempty"` - PieceInterval string `json:"pieceInterval,omitempty"` + JobKeepWindow int `json:"jobKeepWindow,omitempty" example:"5"` + RecoveryWindow int `json:"recoveryWindow,omitempty" example:"3"` + PieceInterval int `json:"pieceInterval,omitempty" example:"1"` } diff --git a/distribution/dashboard/internal/model/param/obtenant_param.go b/distribution/dashboard/internal/model/param/obtenant_param.go index b3a3a5b7e..15bf1a91b 100644 --- a/distribution/dashboard/internal/model/param/obtenant_param.go +++ b/distribution/dashboard/internal/model/param/obtenant_param.go @@ -34,9 +34,9 @@ type TenantSourceSpec struct { type RestoreSourceSpec struct { // Enum: OSS, NFS - Type BackupDestType `json:"type"` - ArchiveSource string `json:"archiveSource"` - BakDataSource string `json:"bakDataSource"` + Type BackupDestType `json:"type" binding:"required"` + ArchiveSource string `json:"archiveSource" binding:"required"` + BakDataSource string `json:"bakDataSource" binding:"required"` OSSAccessID string `json:"ossAccessId,omitempty"` OSSAccessKey string `json:"ossAccessKey,omitempty"` @@ -54,26 +54,21 @@ type UnitConfig struct { } type RestoreUntilConfig struct { - Timestamp *string `json:"timestamp,omitempty"` + Timestamp *string `json:"timestamp,omitempty" example:"2024-02-23 17:47:00"` Unlimited bool `json:"unlimited,omitempty"` } -type ModifyUnitNumber struct { - UnitNumber int `json:"unitNum" binding:"required"` -} - type ChangeUserPassword struct { // Description: The user name of the database account, only root is supported now. - User string `json:"User" binding:"required"` - Password string `json:"Password" binding:"required"` + User string `json:"user" binding:"required"` + Password string `json:"password" binding:"required"` } type ReplayStandbyLog RestoreUntilConfig type ChangeTenantRole struct { - // Enum: Primary, Standby - TenantRole TenantRole `json:"tenantRole" binding:"required"` - Switchover bool `json:"switchover,omitempty"` + Failover bool `json:"failover,omitempty"` + Switchover bool `json:"switchover,omitempty"` } type PatchUnitConfig struct { diff --git a/distribution/dashboard/internal/router/v1/obtenant_router.go b/distribution/dashboard/internal/router/v1/obtenant_router.go index bc06e6e40..c8ee4f1db 100644 --- a/distribution/dashboard/internal/router/v1/obtenant_router.go +++ b/distribution/dashboard/internal/router/v1/obtenant_router.go @@ -7,17 +7,17 @@ import ( func InitOBTenantRoutes(g *gin.RouterGroup) { g.GET("/obtenants", h.Wrap(h.ListAllTenants)) - g.GET("/obtenant/:namespace/:name", h.Wrap(h.GetTenant)) - g.PUT("/obtenant/:namespace/:name", h.Wrap(h.CreateTenant)) - g.DELETE("/obtenant/:namespace/:name", h.Wrap(h.DeleteTenant)) - g.PATCH("/obtenant/:namespace/:name", h.Wrap(h.PatchTenant)) - g.POST("/obtenant/:namespace/:name/userCredentials", h.Wrap(h.ChangeUserPassword)) - g.POST("/obtenant/:namespace/:name/logreplay", h.Wrap(h.ReplayStandbyLog)) - g.POST("/obtenant/:namespace/:name/version", h.Wrap(h.UpgradeTenantVersion)) - g.POST("/obtenant/:namespace/:name/role", h.Wrap(h.ChangeTenantRole)) - g.GET("/obtenant/:namespace/:name/backupPolicy", h.Wrap(h.GetBackupPolicy)) - g.PUT("/obtenant/:namespace/:name/backupPolicy", h.Wrap(h.CreateBackupPolicy)) - g.POST("/obtenant/:namespace/:name/backupPolicy", h.Wrap(h.UpdateBackupPolicy)) - g.DELETE("/obtenant/:namespace/:name/backupPolicy", h.Wrap(h.DeleteBackupPolicy)) - g.GET("/obtenant/:namespace/:name/backup/:type/jobs", h.Wrap(h.ListBackupJobs)) + g.GET("/obtenants/:namespace/:name", h.Wrap(h.GetTenant)) + g.PUT("/obtenants", h.Wrap(h.CreateTenant)) + g.DELETE("/obtenants/:namespace/:name", h.Wrap(h.DeleteTenant)) + g.PATCH("/obtenants/:namespace/:name", h.Wrap(h.PatchTenant)) + g.POST("/obtenants/:namespace/:name/userCredentials", h.Wrap(h.ChangeUserPassword)) + g.POST("/obtenants/:namespace/:name/logreplay", h.Wrap(h.ReplayStandbyLog)) + g.POST("/obtenants/:namespace/:name/version", h.Wrap(h.UpgradeTenantVersion)) + g.POST("/obtenants/:namespace/:name/role", h.Wrap(h.ChangeTenantRole)) + g.GET("/obtenants/:namespace/:name/backupPolicy", h.Wrap(h.GetBackupPolicy)) + g.PUT("/obtenants/:namespace/:name/backupPolicy", h.Wrap(h.CreateBackupPolicy)) + g.PATCH("/obtenants/:namespace/:name/backupPolicy", h.Wrap(h.UpdateBackupPolicy)) + g.DELETE("/obtenants/:namespace/:name/backupPolicy", h.Wrap(h.DeleteBackupPolicy)) + g.GET("/obtenants/:namespace/:name/backup/:type/jobs", h.Wrap(h.ListBackupJobs)) } diff --git a/distribution/dashboard/pkg/errors/error.go b/distribution/dashboard/pkg/errors/error.go index d2261d9f1..976e6bcbc 100644 --- a/distribution/dashboard/pkg/errors/error.go +++ b/distribution/dashboard/pkg/errors/error.go @@ -11,6 +11,8 @@ type httpErr struct { children []*httpErr } +var _ ObError = &httpErr{} + func (e *httpErr) Error() string { return fmt.Sprintf("Error %s: %s", e.errorType, e.message) }