Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a017c71d20 | ||
|
|
4001f14953 | ||
|
|
130413cbef | ||
|
|
0622cc658b | ||
|
|
663a5b196d | ||
|
|
6f676f73a4 | ||
|
|
26d7001ae1 | ||
|
|
f79a8e8177 | ||
|
|
0e5147bb1d | ||
|
|
566fb27fc1 | ||
|
|
8c18829089 | ||
|
|
c68c175827 | ||
|
|
415088ae2d | ||
|
|
158926f192 | ||
|
|
f026ffffc8 | ||
|
|
bd0edea3df | ||
|
|
07b4dad4d3 | ||
|
|
0df796bc03 | ||
|
|
cf0212391e | ||
|
|
cb3a695c25 |
2
.github/workflows/deploy-tenant.sh
vendored
2
.github/workflows/deploy-tenant.sh
vendored
@@ -65,7 +65,7 @@ function main() {
|
||||
|
||||
check_tenant_status tenant-lite storage-lite
|
||||
|
||||
kubectl -n minio-operator port-forward svc/console 9090 &
|
||||
kubectl proxy &
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
2
.github/workflows/jobs.yaml
vendored
2
.github/workflows/jobs.yaml
vendored
@@ -1346,7 +1346,7 @@ jobs:
|
||||
result=${result%\%}
|
||||
echo "result:"
|
||||
echo $result
|
||||
threshold=36.6
|
||||
threshold=41.2
|
||||
if (( $(echo "$result >= $threshold" |bc -l) )); then
|
||||
echo "It is equal or greater than threshold, passed!"
|
||||
else
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,6 +19,7 @@ vendor/
|
||||
|
||||
# Ignore executables
|
||||
target/
|
||||
!pkg/logger/target/
|
||||
console
|
||||
!console/
|
||||
|
||||
|
||||
10
Makefile
10
Makefile
@@ -164,18 +164,12 @@ test-sso-integration:
|
||||
-p 9000:9000 \
|
||||
-p 9001:9001 \
|
||||
-e MINIO_IDENTITY_OPENID_CLIENT_SECRET=0nfJuqIt0iPnRIUJkvetve5l38C6gi9W \
|
||||
-e MINIO_IDENTITY_OPENID_CONFIG_URL=http://keycloak-container:8080/auth/realms/myrealm/.well-known/openid-configuration \
|
||||
-e MINIO_IDENTITY_OPENID_CLIENT_ID="account" \
|
||||
-e MINIO_ROOT_USER=minio \
|
||||
-e MINIO_ROOT_PASSWORD=minio123 $(MINIO_VERSION) server /data{1...4} --address :9000 --console-address :9001)
|
||||
@(sleep 60)
|
||||
@echo "run mc commands"
|
||||
@(docker run --name minio-client --network my-net -dit --entrypoint=/bin/sh minio/mc)
|
||||
@(docker exec minio-client mc alias set myminio/ http://minio:9000 minio minio123)
|
||||
@(docker exec minio-client mc admin config set myminio identity_openid config_url="http://keycloak-container:8080/auth/realms/myrealm/.well-known/openid-configuration" client_id="account")
|
||||
@(docker exec minio-client mc admin service restart myminio)
|
||||
@echo "starting bash script"
|
||||
@(env bash $(PWD)/sso-integration/set-sso.sh)
|
||||
@echo "install jq"
|
||||
@(sudo apt install jq)
|
||||
@echo "Executing the test:"
|
||||
@(cd sso-integration && go test -coverpkg=../restapi -c -tags testrunmain . && mkdir -p coverage && ./sso-integration.test -test.v -test.run "^Test*" -test.coverprofile=coverage/sso-system.out)
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
xhttp "github.com/minio/console/pkg/http"
|
||||
|
||||
"github.com/minio/console/pkg/utils"
|
||||
|
||||
"github.com/minio/pkg/env"
|
||||
@@ -68,7 +70,7 @@ func GetMinioImage() (*string, error) {
|
||||
return &image, nil
|
||||
}
|
||||
latestMinIOImage, errLatestMinIOImage := utils.GetLatestMinIOImage(
|
||||
&utils.HTTPClient{
|
||||
&xhttp.Client{
|
||||
Client: &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
},
|
||||
|
||||
@@ -20,10 +20,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/minio/console/pkg/logger"
|
||||
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/console/restapi"
|
||||
)
|
||||
@@ -36,15 +40,28 @@ var appCmds = []cli.Command{
|
||||
|
||||
// StartServer starts the console service
|
||||
func StartServer(ctx *cli.Context) error {
|
||||
if os.Getenv("CONSOLE_OPERATOR_MODE") != "" && os.Getenv("CONSOLE_OPERATOR_MODE") == "on" {
|
||||
return startOperatorServer(ctx)
|
||||
}
|
||||
|
||||
// Load all certificates
|
||||
if err := loadAllCerts(ctx); err != nil {
|
||||
// Log this as a warning and continue running console without TLS certificates
|
||||
restapi.LogError("Unable to load certs: %v", err)
|
||||
}
|
||||
|
||||
xctx := context.Background()
|
||||
transport := restapi.PrepareSTSClientTransport(false)
|
||||
if err := logger.InitializeLogger(xctx, transport); err != nil {
|
||||
fmt.Println("error InitializeLogger", err)
|
||||
logger.CriticalIf(xctx, err)
|
||||
}
|
||||
// custom error configuration
|
||||
restapi.LogInfo = logger.Info
|
||||
restapi.LogError = logger.Error
|
||||
restapi.LogIf = logger.LogIf
|
||||
|
||||
if os.Getenv("CONSOLE_OPERATOR_MODE") != "" && os.Getenv("CONSOLE_OPERATOR_MODE") == "on" {
|
||||
return startOperatorServer(ctx)
|
||||
}
|
||||
|
||||
var rctx restapi.Context
|
||||
if err := rctx.Load(ctx); err != nil {
|
||||
restapi.LogError("argument validation failed: %v", err)
|
||||
|
||||
@@ -20,9 +20,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/minio/console/pkg/logger"
|
||||
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/console/restapi"
|
||||
)
|
||||
@@ -39,6 +43,17 @@ func StartServer(ctx *cli.Context) error {
|
||||
restapi.LogError("Unable to load certs: %v", err)
|
||||
}
|
||||
|
||||
xctx := context.Background()
|
||||
transport := restapi.PrepareSTSClientTransport(false)
|
||||
if err := logger.InitializeLogger(xctx, transport); err != nil {
|
||||
fmt.Println("error InitializeLogger", err)
|
||||
logger.CriticalIf(xctx, err)
|
||||
}
|
||||
// custom error configuration
|
||||
restapi.LogInfo = logger.Info
|
||||
restapi.LogError = logger.Error
|
||||
restapi.LogIf = logger.LogIf
|
||||
|
||||
var rctx restapi.Context
|
||||
if err := rctx.Load(ctx); err != nil {
|
||||
restapi.LogError("argument validation failed: %v", err)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// +build operator
|
||||
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -20,6 +20,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
@@ -27,6 +28,8 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/minio/console/pkg/logger"
|
||||
|
||||
"github.com/minio/console/restapi"
|
||||
|
||||
"github.com/go-openapi/loads"
|
||||
@@ -106,7 +109,7 @@ func buildOperatorServer() (*operatorapi.Server, error) {
|
||||
}
|
||||
|
||||
api := operations.NewOperatorAPI(swaggerSpec)
|
||||
api.Logger = operatorapi.LogInfo
|
||||
api.Logger = restapi.LogInfo
|
||||
server := operatorapi.NewServer(api)
|
||||
|
||||
parser := flags.NewParser(server, flags.Default)
|
||||
@@ -147,7 +150,7 @@ func loadOperatorAllCerts(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
// load the certificates and the CAs
|
||||
operatorapi.GlobalRootCAs, operatorapi.GlobalPublicCerts, operatorapi.GlobalTLSCertsManager, err = certs.GetAllCertificatesAndCAs()
|
||||
restapi.GlobalRootCAs, restapi.GlobalPublicCerts, restapi.GlobalTLSCertsManager, err = certs.GetAllCertificatesAndCAs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load certificates at %s: failed with %w", certs.GlobalCertsDir.Get(), err)
|
||||
}
|
||||
@@ -159,12 +162,12 @@ func loadOperatorAllCerts(ctx *cli.Context) error {
|
||||
swaggerServerCACertificate := ctx.String("tls-ca")
|
||||
// load tls cert and key from swagger server tls-certificate and tls-key flags
|
||||
if swaggerServerCertificate != "" && swaggerServerCertificateKey != "" {
|
||||
if err = operatorapi.GlobalTLSCertsManager.AddCertificate(swaggerServerCertificate, swaggerServerCertificateKey); err != nil {
|
||||
if err = restapi.GlobalTLSCertsManager.AddCertificate(swaggerServerCertificate, swaggerServerCertificateKey); err != nil {
|
||||
return err
|
||||
}
|
||||
x509Certs, err := certs.ParsePublicCertFile(swaggerServerCertificate)
|
||||
if err == nil {
|
||||
operatorapi.GlobalPublicCerts = append(operatorapi.GlobalPublicCerts, x509Certs...)
|
||||
restapi.GlobalPublicCerts = append(restapi.GlobalPublicCerts, x509Certs...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +175,7 @@ func loadOperatorAllCerts(ctx *cli.Context) error {
|
||||
if swaggerServerCACertificate != "" {
|
||||
caCert, caCertErr := ioutil.ReadFile(swaggerServerCACertificate)
|
||||
if caCertErr == nil {
|
||||
operatorapi.GlobalRootCAs.AppendCertsFromPEM(caCert)
|
||||
restapi.GlobalRootCAs.AppendCertsFromPEM(caCert)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,20 +189,32 @@ func loadOperatorAllCerts(ctx *cli.Context) error {
|
||||
|
||||
// StartServer starts the console service
|
||||
func startOperatorServer(ctx *cli.Context) error {
|
||||
if err := loadOperatorAllCerts(ctx); err != nil {
|
||||
|
||||
if err := loadAllCerts(ctx); err != nil {
|
||||
// Log this as a warning and continue running console without TLS certificates
|
||||
operatorapi.LogError("Unable to load certs: %v", err)
|
||||
restapi.LogError("Unable to load certs: %v", err)
|
||||
}
|
||||
|
||||
xctx := context.Background()
|
||||
transport := restapi.PrepareSTSClientTransport(false)
|
||||
if err := logger.InitializeLogger(xctx, transport); err != nil {
|
||||
fmt.Println("error InitializeLogger", err)
|
||||
logger.CriticalIf(xctx, err)
|
||||
}
|
||||
// custom error configuration
|
||||
restapi.LogInfo = logger.Info
|
||||
restapi.LogError = logger.Error
|
||||
restapi.LogIf = logger.LogIf
|
||||
|
||||
var rctx operatorapi.Context
|
||||
if err := rctx.Load(ctx); err != nil {
|
||||
operatorapi.LogError("argument validation failed: %v", err)
|
||||
restapi.LogError("argument validation failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
server, err := buildOperatorServer()
|
||||
if err != nil {
|
||||
operatorapi.LogError("Unable to initialize console server: %v", err)
|
||||
restapi.LogError("Unable to initialize console server: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -212,7 +227,7 @@ func startOperatorServer(ctx *cli.Context) error {
|
||||
operatorapi.Port = strconv.Itoa(server.Port)
|
||||
operatorapi.Hostname = server.Host
|
||||
|
||||
if len(operatorapi.GlobalPublicCerts) > 0 {
|
||||
if len(restapi.GlobalPublicCerts) > 0 {
|
||||
// If TLS certificates are provided enforce the HTTPS schema, meaning console will redirect
|
||||
// plain HTTP connections to HTTPS server
|
||||
server.EnabledListeners = []string{"http", "https"}
|
||||
|
||||
9
go.mod
9
go.mod
@@ -6,6 +6,7 @@ require (
|
||||
github.com/blang/semver/v4 v4.0.0
|
||||
github.com/cheggaaa/pb/v3 v3.0.8
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/go-openapi/errors v0.20.2
|
||||
github.com/go-openapi/loads v0.21.1
|
||||
github.com/go-openapi/runtime v0.23.3
|
||||
@@ -14,10 +15,12 @@ require (
|
||||
github.com/go-openapi/swag v0.21.1
|
||||
github.com/go-openapi/validate v0.21.0
|
||||
github.com/golang-jwt/jwt/v4 v4.4.1
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/jessevdk/go-flags v1.5.0
|
||||
github.com/klauspost/compress v1.15.1
|
||||
github.com/minio/cli v1.22.0
|
||||
github.com/minio/highwayhash v1.0.2
|
||||
github.com/minio/kes v0.19.2
|
||||
github.com/minio/madmin-go v1.3.12
|
||||
github.com/minio/mc v0.0.0-20220419155441-cc4ff3a0cc82
|
||||
@@ -57,7 +60,6 @@ require (
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 // indirect
|
||||
@@ -74,7 +76,6 @@ require (
|
||||
github.com/google/go-cmp v0.5.7 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/googleapis/gnostic v0.5.5 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
@@ -109,7 +110,7 @@ require (
|
||||
github.com/navidys/tvxwidgets v0.1.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/philhofer/fwd v1.1.1 // indirect
|
||||
github.com/philhofer/fwd v1.1.2-0.20210722190033-5c56ac6d0bb9 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pkg/xattr v0.4.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
@@ -126,7 +127,7 @@ require (
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tinylib/msgp v1.1.6 // indirect
|
||||
github.com/tinylib/msgp v1.1.7-0.20211026165309-e818a1881b0e // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||
github.com/tklauser/numcpus v0.4.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
|
||||
83
integration/config_test.go
Normal file
83
integration/config_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_ConfigAPI(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
type args struct {
|
||||
api string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expectedStatus int
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Config - Valid",
|
||||
args: args{
|
||||
api: "/configs",
|
||||
},
|
||||
expectedStatus: 200,
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
requestDataPolicy := map[string]interface{}{}
|
||||
|
||||
requestDataJSON, _ := json.Marshal(requestDataPolicy)
|
||||
requestDataBody := bytes.NewReader(requestDataJSON)
|
||||
request, err := http.NewRequest(
|
||||
"GET", fmt.Sprintf("http://localhost:9090/api/v1%s", tt.args.api), requestDataBody)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if response != nil {
|
||||
assert.Equal(tt.expectedStatus, response.StatusCode, tt.name+" Failed")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
174
integration/groups_test.go
Normal file
174
integration/groups_test.go
Normal file
@@ -0,0 +1,174 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_AddGroupAPI(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
AddUser("member1", "testtest", []string{}, []string{"consoleAdmin"})
|
||||
|
||||
type args struct {
|
||||
api string
|
||||
group string
|
||||
members []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expectedStatus int
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Create Group - Valid",
|
||||
args: args{
|
||||
api: "/groups",
|
||||
group: "test",
|
||||
members: []string{"member1"},
|
||||
},
|
||||
expectedStatus: 201,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Create Group - Invalid",
|
||||
args: args{
|
||||
api: "/groups",
|
||||
group: "test",
|
||||
members: []string{},
|
||||
},
|
||||
expectedStatus: 400,
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
// Add policy
|
||||
|
||||
requestDataPolicy := map[string]interface{}{}
|
||||
requestDataPolicy["group"] = tt.args.group
|
||||
requestDataPolicy["members"] = tt.args.members
|
||||
|
||||
requestDataJSON, _ := json.Marshal(requestDataPolicy)
|
||||
requestDataBody := bytes.NewReader(requestDataJSON)
|
||||
request, err := http.NewRequest(
|
||||
"POST", fmt.Sprintf("http://localhost:9090/api/v1%s", tt.args.api), requestDataBody)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if response != nil {
|
||||
assert.Equal(tt.expectedStatus, response.StatusCode, "Status Code is incorrect")
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_DeleteGroupAPI(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
AddGroup("grouptests1", []string{})
|
||||
|
||||
type args struct {
|
||||
api string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expectedStatus int
|
||||
expectedError error
|
||||
verb string
|
||||
}{
|
||||
{
|
||||
name: "Delete Group - Valid",
|
||||
verb: "DELETE",
|
||||
args: args{
|
||||
api: "/group?name=grouptests1",
|
||||
},
|
||||
expectedStatus: 204,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Access Group After Delete - Invalid",
|
||||
verb: "GET",
|
||||
args: args{
|
||||
api: "/group?name=grouptests1",
|
||||
},
|
||||
expectedStatus: 500,
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
// Add policy
|
||||
|
||||
requestDataPolicy := map[string]interface{}{}
|
||||
|
||||
requestDataJSON, _ := json.Marshal(requestDataPolicy)
|
||||
requestDataBody := bytes.NewReader(requestDataJSON)
|
||||
request, err := http.NewRequest(
|
||||
tt.verb, fmt.Sprintf("http://localhost:9090/api/v1%s", tt.args.api), requestDataBody)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if response != nil {
|
||||
assert.Equal(tt.expectedStatus, response.StatusCode, "Status Code is incorrect")
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"testing"
|
||||
@@ -30,7 +31,64 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_PolicyAPI(t *testing.T) {
|
||||
func AddPolicy(name string, definition string) (*http.Response, error) {
|
||||
/*
|
||||
This is an atomic function to add user and can be reused across
|
||||
different functions.
|
||||
*/
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
requestDataAdd := map[string]interface{}{
|
||||
"name": name,
|
||||
"policy": definition,
|
||||
}
|
||||
|
||||
requestDataJSON, _ := json.Marshal(requestDataAdd)
|
||||
requestDataBody := bytes.NewReader(requestDataJSON)
|
||||
request, err := http.NewRequest(
|
||||
"POST", "http://localhost:9090/api/v1/policies", requestDataBody)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
|
||||
response, err := client.Do(request)
|
||||
return response, err
|
||||
}
|
||||
|
||||
func SetPolicy(policies []string, entityName string, entityType string) (*http.Response, error) {
|
||||
/*
|
||||
This is an atomic function to add user and can be reused across
|
||||
different functions.
|
||||
*/
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
requestDataAdd := map[string]interface{}{
|
||||
"name": policies,
|
||||
"entityType": entityType,
|
||||
"entityName": entityName,
|
||||
}
|
||||
|
||||
requestDataJSON, _ := json.Marshal(requestDataAdd)
|
||||
requestDataBody := bytes.NewReader(requestDataJSON)
|
||||
request, err := http.NewRequest(
|
||||
"PUT", "http://localhost:9090/api/v1/set-policy", requestDataBody)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
|
||||
response, err := client.Do(request)
|
||||
return response, err
|
||||
}
|
||||
|
||||
func Test_AddPolicyAPI(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
type args struct {
|
||||
@@ -69,11 +127,12 @@ func Test_PolicyAPI(t *testing.T) {
|
||||
expectedStatus: 201,
|
||||
expectedError: nil,
|
||||
},
|
||||
|
||||
{
|
||||
name: "Create Policy - Invalid",
|
||||
args: args{
|
||||
api: "/policies",
|
||||
name: "test",
|
||||
name: "test2",
|
||||
policy: swag.String(`
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
@@ -103,11 +162,9 @@ func Test_PolicyAPI(t *testing.T) {
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
// Add policy
|
||||
|
||||
requestDataPolicy := map[string]interface{}{}
|
||||
requestDataPolicy["name"] = tt.args.name
|
||||
if tt.args.policy != nil {
|
||||
requestDataPolicy["name"] = tt.args.name
|
||||
requestDataPolicy["policy"] = *tt.args.policy
|
||||
}
|
||||
|
||||
@@ -127,10 +184,604 @@ func Test_PolicyAPI(t *testing.T) {
|
||||
return
|
||||
}
|
||||
if response != nil {
|
||||
assert.Equal(tt.expectedStatus, response.StatusCode, "Status Code is incorrect")
|
||||
assert.Equal(tt.expectedStatus, response.StatusCode, tt.name+" Failed")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_SetPolicyAPI(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
AddUser("policyuser1", "testtest", []string{}, []string{"readwrite"})
|
||||
AddGroup("testgroup123", []string{})
|
||||
AddPolicy("setpolicytest", `
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:GetBucketLocation",
|
||||
"s3:GetObject"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
type args struct {
|
||||
api string
|
||||
entityType string
|
||||
entityName string
|
||||
policyName []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expectedStatus int
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Set Policy - Valid",
|
||||
args: args{
|
||||
api: "/set-policy",
|
||||
policyName: []string{"setpolicytest"},
|
||||
entityType: "user",
|
||||
entityName: "policyuser1",
|
||||
},
|
||||
expectedStatus: 204,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Set Policy - Invalid",
|
||||
args: args{
|
||||
api: "/set-policy",
|
||||
policyName: []string{"test3"},
|
||||
entityType: "user",
|
||||
entityName: "policyuser1",
|
||||
},
|
||||
expectedStatus: 500,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Set Policy Group - Valid",
|
||||
args: args{
|
||||
api: "/set-policy",
|
||||
policyName: []string{"setpolicytest"},
|
||||
entityType: "group",
|
||||
entityName: "testgroup123",
|
||||
},
|
||||
expectedStatus: 204,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Set Policy Group - Invalid",
|
||||
args: args{
|
||||
api: "/set-policy",
|
||||
policyName: []string{"test3"},
|
||||
entityType: "group",
|
||||
entityName: "testgroup123",
|
||||
},
|
||||
expectedStatus: 500,
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
requestDataPolicy := map[string]interface{}{}
|
||||
requestDataPolicy["entityName"] = tt.args.entityName
|
||||
requestDataPolicy["entityType"] = tt.args.entityType
|
||||
if tt.args.policyName != nil {
|
||||
requestDataPolicy["name"] = tt.args.policyName
|
||||
}
|
||||
|
||||
requestDataJSON, _ := json.Marshal(requestDataPolicy)
|
||||
requestDataBody := bytes.NewReader(requestDataJSON)
|
||||
request, err := http.NewRequest(
|
||||
"PUT", fmt.Sprintf("http://localhost:9090/api/v1%s", tt.args.api), requestDataBody)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if response != nil {
|
||||
assert.Equal(tt.expectedStatus, response.StatusCode, tt.name+" Failed")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_SetPolicyMultipleAPI(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
AddUser("policyuser2", "testtest", []string{}, []string{"readwrite"})
|
||||
AddUser("policyuser3", "testtest", []string{}, []string{"readwrite"})
|
||||
AddGroup("testgroup1234", []string{})
|
||||
AddPolicy("setpolicytest2", `
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:GetBucketLocation",
|
||||
"s3:GetObject"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
type args struct {
|
||||
api string
|
||||
users []string
|
||||
groups []string
|
||||
name []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expectedStatus int
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Set Policy - Valid",
|
||||
args: args{
|
||||
api: "/set-policy-multi",
|
||||
name: []string{"setpolicytest2"},
|
||||
users: []string{"policyuser2", "policyuser3"},
|
||||
},
|
||||
expectedStatus: 204,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Set Policy - Invalid",
|
||||
args: args{
|
||||
api: "/set-policy-multi",
|
||||
name: []string{"test3"},
|
||||
users: []string{"policyuser2", "policyuser3"},
|
||||
},
|
||||
expectedStatus: 500,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Set Policy Group - Valid",
|
||||
args: args{
|
||||
api: "/set-policy-multi",
|
||||
name: []string{"setpolicytest2"},
|
||||
groups: []string{"testgroup1234"},
|
||||
},
|
||||
expectedStatus: 204,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Set Policy Group - Valid",
|
||||
args: args{
|
||||
api: "/set-policy-multi",
|
||||
name: []string{"setpolicytest23"},
|
||||
groups: []string{"testgroup1234"},
|
||||
},
|
||||
expectedStatus: 500,
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
requestDataPolicy := map[string]interface{}{}
|
||||
requestDataPolicy["name"] = tt.args.name
|
||||
requestDataPolicy["users"] = tt.args.users
|
||||
requestDataPolicy["groups"] = tt.args.groups
|
||||
|
||||
requestDataJSON, _ := json.Marshal(requestDataPolicy)
|
||||
requestDataBody := bytes.NewReader(requestDataJSON)
|
||||
request, err := http.NewRequest(
|
||||
"PUT", fmt.Sprintf("http://localhost:9090/api/v1%s", tt.args.api), requestDataBody)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if response != nil {
|
||||
assert.Equal(tt.expectedStatus, response.StatusCode, tt.name+" Failed")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_ListPoliciesAPI(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
type args struct {
|
||||
api string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expectedStatus int
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "List Policies",
|
||||
args: args{
|
||||
api: "/policies",
|
||||
},
|
||||
expectedStatus: 200,
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(
|
||||
"GET", fmt.Sprintf("http://localhost:9090/api/v1%s", tt.args.api), nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if response != nil {
|
||||
assert.Equal(tt.expectedStatus, response.StatusCode, tt.name+" Failed")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_GetPolicyAPI(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
AddPolicy("getpolicytest", `
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:GetBucketLocation",
|
||||
"s3:GetObject"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
type args struct {
|
||||
api string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expectedStatus int
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Get Policies - Invalid",
|
||||
args: args{
|
||||
api: "/policy?name=test3",
|
||||
},
|
||||
expectedStatus: 500,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Get Policies - Valid",
|
||||
args: args{
|
||||
api: "/policy?name=getpolicytest",
|
||||
},
|
||||
expectedStatus: 200,
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(
|
||||
"GET", fmt.Sprintf("http://localhost:9090/api/v1%s", tt.args.api), nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if response != nil {
|
||||
assert.Equal(tt.expectedStatus, response.StatusCode, tt.name+" Failed")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_PolicyListUsersAPI(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
AddUser("policyuser4", "testtest", []string{}, []string{"readwrite"})
|
||||
AddPolicy("policylistusers", `
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:GetBucketLocation",
|
||||
"s3:GetObject"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
SetPolicy([]string{"policylistusers"}, "policyuser4", "user")
|
||||
|
||||
type args struct {
|
||||
api string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expectedStatus int
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "List Users for Policy - Valid",
|
||||
args: args{
|
||||
api: "/policies/policylistusers/users",
|
||||
},
|
||||
expectedStatus: 200,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "List Users for Policy - Invalid",
|
||||
args: args{
|
||||
api: "/policies/test2/users",
|
||||
},
|
||||
expectedStatus: 404,
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(
|
||||
"GET", fmt.Sprintf("http://localhost:9090/api/v1%s", tt.args.api), nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if response != nil {
|
||||
bodyBytes, _ := ioutil.ReadAll(response.Body)
|
||||
assert.Equal(tt.expectedStatus, response.StatusCode, tt.name+" Failed")
|
||||
if response.StatusCode == 200 {
|
||||
assert.Equal("[\"policyuser4\"]\n", string(bodyBytes))
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_PolicyListGroupsAPI(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
AddGroup("testgroup12345", []string{})
|
||||
AddPolicy("policylistgroups", `
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:GetBucketLocation",
|
||||
"s3:GetObject"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
SetPolicy([]string{"policylistgroups"}, "testgroup12345", "group")
|
||||
|
||||
type args struct {
|
||||
api string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expectedStatus int
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "List Users for Policy - Valid",
|
||||
args: args{
|
||||
api: "/policies/policylistgroups/groups",
|
||||
},
|
||||
expectedStatus: 200,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "List Users for Policy - Invalid",
|
||||
args: args{
|
||||
api: "/policies/test3/groups",
|
||||
},
|
||||
expectedStatus: 404,
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(
|
||||
"GET", fmt.Sprintf("http://localhost:9090/api/v1%s", tt.args.api), nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if response != nil {
|
||||
bodyBytes, _ := ioutil.ReadAll(response.Body)
|
||||
assert.Equal(tt.expectedStatus, response.StatusCode, tt.name+" Failed")
|
||||
if response.StatusCode == 200 {
|
||||
assert.Equal("[\"testgroup12345\"]\n", string(bodyBytes))
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_DeletePolicyAPI(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
AddPolicy("testdelete", `
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:GetBucketLocation",
|
||||
"s3:GetObject"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
type args struct {
|
||||
api string
|
||||
method string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expectedStatus int
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Delete Policies - Valid",
|
||||
args: args{
|
||||
api: "/policy?name=testdelete",
|
||||
method: "DELETE",
|
||||
},
|
||||
expectedStatus: 204,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Get Policy After Delete - Invalid",
|
||||
args: args{
|
||||
api: "/policy?name=testdelete",
|
||||
method: "GET",
|
||||
},
|
||||
expectedStatus: 500,
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(
|
||||
tt.args.method, fmt.Sprintf("http://localhost:9090/api/v1%s", tt.args.api), nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if response != nil {
|
||||
assert.Equal(tt.expectedStatus, response.StatusCode, tt.name+" Failed")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
121
integration/profiling_test.go
Normal file
121
integration/profiling_test.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStartProfiling(t *testing.T) {
|
||||
testAsser := assert.New(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
}{
|
||||
{
|
||||
name: "start/stop profiling",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
files := map[string]bool{
|
||||
"profile-127.0.0.1:9000-goroutines.txt": false,
|
||||
"profile-127.0.0.1:9000-goroutines-before.txt": false,
|
||||
"profile-127.0.0.1:9000-goroutines-before,debug=2.txt": false,
|
||||
"profile-127.0.0.1:9000-threads-before.pprof": false,
|
||||
"profile-127.0.0.1:9000-mem.pprof": false,
|
||||
"profile-127.0.0.1:9000-threads.pprof": false,
|
||||
"profile-127.0.0.1:9000-cpu.pprof": false,
|
||||
"profile-127.0.0.1:9000-mem-before.pprof": false,
|
||||
"profile-127.0.0.1:9000-block.pprof": false,
|
||||
"profile-127.0.0.1:9000-trace.trace": false,
|
||||
"profile-127.0.0.1:9000-mutex.pprof": false,
|
||||
"profile-127.0.0.1:9000-mutex-before.pprof": false,
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
destination := "/api/v1/profiling/start"
|
||||
finalURL := fmt.Sprintf("http://localhost:9090%s", destination)
|
||||
request, err := http.NewRequest("POST", finalURL, strings.NewReader("{\"type\":\"cpu,mem,block,mutex,trace,threads,goroutines\"}"))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
|
||||
response, err := client.Do(request)
|
||||
|
||||
testAsser.Nil(err, fmt.Sprintf("%s returned an error: %v", tt.name, err))
|
||||
testAsser.Equal(201, response.StatusCode)
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
destination = "/api/v1/profiling/stop"
|
||||
finalURL = fmt.Sprintf("http://localhost:9090%s", destination)
|
||||
request, err = http.NewRequest("POST", finalURL, nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
|
||||
response, err = client.Do(request)
|
||||
|
||||
testAsser.Nil(err, fmt.Sprintf("%s returned an error: %v", tt.name, err))
|
||||
testAsser.Equal(200, response.StatusCode)
|
||||
zipFileBytes, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
testAsser.Nil(err, fmt.Sprintf("%s returned an error: %v", tt.name, err))
|
||||
}
|
||||
filetype := http.DetectContentType(zipFileBytes)
|
||||
testAsser.Equal("application/zip", filetype)
|
||||
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(zipFileBytes), int64(len(zipFileBytes)))
|
||||
if err != nil {
|
||||
testAsser.Nil(err, fmt.Sprintf("%s returned an error: %v", tt.name, err))
|
||||
}
|
||||
|
||||
// Read all the files from zip archive
|
||||
for _, zipFile := range zipReader.File {
|
||||
files[zipFile.Name] = true
|
||||
}
|
||||
|
||||
for k, v := range files {
|
||||
testAsser.Equal(true, v, fmt.Sprintf("%s : compressed file expected to have %v file inside", tt.name, k))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ spec:
|
||||
serviceAccountName: console-sa
|
||||
containers:
|
||||
- name: console
|
||||
image: 'minio/console:v0.16.0'
|
||||
image: 'minio/console:v0.16.1'
|
||||
imagePullPolicy: "IfNotPresent"
|
||||
env:
|
||||
- name: CONSOLE_OPERATOR_MODE
|
||||
|
||||
@@ -32,7 +32,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: console
|
||||
image: 'minio/console:v0.16.0'
|
||||
image: 'minio/console:v0.16.1'
|
||||
imagePullPolicy: "IfNotPresent"
|
||||
env:
|
||||
- name: CONSOLE_MINIO_SERVER
|
||||
|
||||
@@ -18,15 +18,14 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
errors "github.com/minio/console/restapi"
|
||||
|
||||
"github.com/minio/console/cluster"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
operatorClientset "github.com/minio/operator/pkg/client/clientset/versioned"
|
||||
)
|
||||
|
||||
var errInvalidCredentials = errors.New("invalid Login")
|
||||
|
||||
// operatorCredentialsProvider is an struct to hold the JWT (service account token)
|
||||
type operatorCredentialsProvider struct {
|
||||
serviceAccountJWT string
|
||||
@@ -86,7 +85,7 @@ func GetConsoleCredentialsForOperator(jwt string) (*credentials.Credentials, err
|
||||
client: opClientClientSet,
|
||||
}
|
||||
if err = checkServiceAccountTokenValid(ctx, opClient); err != nil {
|
||||
return nil, errInvalidCredentials
|
||||
return nil, errors.ErrInvalidLogin
|
||||
}
|
||||
return credentials.New(operatorCredentialsProvider{serviceAccountJWT: jwt}), nil
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/klauspost/compress/gzhttp"
|
||||
|
||||
"github.com/minio/console/restapi"
|
||||
"github.com/unrolled/secure"
|
||||
|
||||
@@ -58,7 +57,7 @@ func configureAPI(api *operations.OperatorAPI) http.Handler {
|
||||
api.KeyAuth = func(token string, scopes []string) (*models.Principal, error) {
|
||||
// we are validating the session token by decrypting the claims inside, if the operation succeed that means the jwt
|
||||
// was generated and signed by us in the first place
|
||||
claims, err := auth.SessionTokenAuthenticate(token)
|
||||
claims, err := auth.ParseClaimsFromToken(token)
|
||||
if err != nil {
|
||||
api.Logger("Unable to validate the session token %s: %v", token, err)
|
||||
return nil, errors.New(401, "incorrect api key auth")
|
||||
@@ -101,8 +100,8 @@ func configureAPI(api *operations.OperatorAPI) http.Handler {
|
||||
|
||||
// The TLS configuration before HTTPS server starts.
|
||||
func configureTLS(tlsConfig *tls.Config) {
|
||||
tlsConfig.RootCAs = GlobalRootCAs
|
||||
tlsConfig.GetCertificate = GlobalTLSCertsManager.GetCertificate
|
||||
tlsConfig.RootCAs = restapi.GlobalRootCAs
|
||||
tlsConfig.GetCertificate = restapi.GlobalTLSCertsManager.GetCertificate
|
||||
}
|
||||
|
||||
// As soon as server is initialized but not run yet, this function will be called.
|
||||
@@ -118,24 +117,6 @@ func setupMiddlewares(handler http.Handler) http.Handler {
|
||||
return handler
|
||||
}
|
||||
|
||||
func AuthenticationMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token, err := auth.GetTokenFromRequest(r)
|
||||
if err != nil && err != auth.ErrNoAuthToken {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
// All handlers handle appropriately to return errors
|
||||
// based on their swagger rules, we do not need to
|
||||
// additionally return error here, let the next ServeHTTPs
|
||||
// handle it appropriately.
|
||||
if token != "" {
|
||||
r.Header.Add("Authorization", "Bearer "+token)
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// proxyMiddleware adds the proxy capability
|
||||
func proxyMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -150,19 +131,23 @@ func proxyMiddleware(next http.Handler) http.Handler {
|
||||
// The middleware configuration happens before anything, this middleware also applies to serving the swagger.json document.
|
||||
// So this is a good place to plug in a panic handling middleware, logging and metrics.
|
||||
func setupGlobalMiddleware(handler http.Handler) http.Handler {
|
||||
// handle cookie or authorization header for session
|
||||
next := AuthenticationMiddleware(handler)
|
||||
// proxy requests
|
||||
next = proxyMiddleware(next)
|
||||
next := proxyMiddleware(handler)
|
||||
// if audit-log is enabled console will log all incoming request
|
||||
next = restapi.AuditLogMiddleware(next)
|
||||
// serve static files
|
||||
next = restapi.FileServerMiddleware(next)
|
||||
// add information to request context
|
||||
next = restapi.ContextMiddleware(next)
|
||||
// handle cookie or authorization header for session
|
||||
next = restapi.AuthenticationMiddleware(next)
|
||||
// Secure middleware, this middleware wrap all the previous handlers and add
|
||||
// HTTP security headers
|
||||
secureOptions := secure.Options{
|
||||
AllowedHosts: restapi.GetSecureAllowedHosts(),
|
||||
AllowedHostsAreRegex: restapi.GetSecureAllowedHostsAreRegex(),
|
||||
HostsProxyHeaders: restapi.GetSecureHostsProxyHeaders(),
|
||||
SSLRedirect: restapi.GetTLSRedirect() == "on" && len(GlobalPublicCerts) > 0,
|
||||
SSLRedirect: restapi.GetTLSRedirect() == "on" && len(restapi.GlobalPublicCerts) > 0,
|
||||
SSLHost: restapi.GetSecureTLSHost(),
|
||||
STSSeconds: restapi.GetSecureSTSSeconds(),
|
||||
STSIncludeSubdomains: restapi.GetSecureSTSIncludeSubdomains(),
|
||||
|
||||
@@ -208,7 +208,7 @@ func init() {
|
||||
"get": {
|
||||
"security": [],
|
||||
"tags": [
|
||||
"UserAPI"
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Returns login strategy, form or sso.",
|
||||
"operationId": "LoginDetail",
|
||||
@@ -232,7 +232,7 @@ func init() {
|
||||
"post": {
|
||||
"security": [],
|
||||
"tags": [
|
||||
"UserAPI"
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Identity Provider oauth2 callback endpoint.",
|
||||
"operationId": "LoginOauth2Auth",
|
||||
@@ -263,7 +263,7 @@ func init() {
|
||||
"post": {
|
||||
"security": [],
|
||||
"tags": [
|
||||
"UserAPI"
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Login to Operator Console.",
|
||||
"operationId": "LoginOperator",
|
||||
@@ -293,7 +293,7 @@ func init() {
|
||||
"/logout": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"UserAPI"
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Logout from Operator.",
|
||||
"operationId": "Logout",
|
||||
@@ -1665,7 +1665,7 @@ func init() {
|
||||
"/session": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"UserAPI"
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Endpoint to check if your session is still valid",
|
||||
"operationId": "SessionCheck",
|
||||
@@ -4278,7 +4278,7 @@ func init() {
|
||||
"get": {
|
||||
"security": [],
|
||||
"tags": [
|
||||
"UserAPI"
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Returns login strategy, form or sso.",
|
||||
"operationId": "LoginDetail",
|
||||
@@ -4302,7 +4302,7 @@ func init() {
|
||||
"post": {
|
||||
"security": [],
|
||||
"tags": [
|
||||
"UserAPI"
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Identity Provider oauth2 callback endpoint.",
|
||||
"operationId": "LoginOauth2Auth",
|
||||
@@ -4333,7 +4333,7 @@ func init() {
|
||||
"post": {
|
||||
"security": [],
|
||||
"tags": [
|
||||
"UserAPI"
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Login to Operator Console.",
|
||||
"operationId": "LoginOperator",
|
||||
@@ -4363,7 +4363,7 @@ func init() {
|
||||
"/logout": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"UserAPI"
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Logout from Operator.",
|
||||
"operationId": "Logout",
|
||||
@@ -5735,7 +5735,7 @@ func init() {
|
||||
"/session": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"UserAPI"
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Endpoint to check if your session is still valid",
|
||||
"operationId": "SessionCheck",
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package operatorapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/swag"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/madmin-go"
|
||||
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// Generic error messages
|
||||
errorGeneric = errors.New("an error occurred, please try again")
|
||||
errInvalidCredentials = errors.New("invalid Login")
|
||||
errorGenericInvalidSession = errors.New("invalid session")
|
||||
errorGenericUnauthorized = errors.New("unauthorized")
|
||||
errorGenericForbidden = errors.New("forbidden")
|
||||
// ErrorGenericNotFound Generic error for not found
|
||||
ErrorGenericNotFound = errors.New("not found")
|
||||
// Explicit error messages
|
||||
errorInvalidErasureCodingValue = errors.New("invalid Erasure Coding Value")
|
||||
errorUnableToGetTenantUsage = errors.New("unable to get tenant usage")
|
||||
errorUnableToGetTenantLogs = errors.New("unable to get tenant logs")
|
||||
errorUnableToUpdateTenantCertificates = errors.New("unable to update tenant certificates")
|
||||
errorUpdatingEncryptionConfig = errors.New("unable to update encryption configuration")
|
||||
errorDeletingEncryptionConfig = errors.New("error disabling tenant encryption")
|
||||
errorEncryptionConfigNotFound = errors.New("encryption configuration not found")
|
||||
errBucketBodyNotInRequest = errors.New("error bucket body not in request")
|
||||
errBucketNameNotInRequest = errors.New("error bucket name not in request")
|
||||
errGroupBodyNotInRequest = errors.New("error group body not in request")
|
||||
errGroupNameNotInRequest = errors.New("error group name not in request")
|
||||
errPolicyNameNotInRequest = errors.New("error policy name not in request")
|
||||
errPolicyBodyNotInRequest = errors.New("error policy body not in request")
|
||||
errSSENotConfigured = errors.New("error server side encryption configuration not found")
|
||||
errBucketLifeCycleNotConfigured = errors.New("error bucket life cycle configuration not found")
|
||||
errChangePassword = errors.New("error please check your current password")
|
||||
errInvalidLicense = errors.New("invalid license key")
|
||||
errLicenseNotFound = errors.New("license not found")
|
||||
errAvoidSelfAccountDelete = errors.New("logged in user cannot be deleted by itself")
|
||||
errAccessDenied = errors.New("access denied")
|
||||
errTooManyNodes = errors.New("cannot request more nodes than what is available in the cluster")
|
||||
errTooFewNodes = errors.New("there are not enough nodes in the cluster to support this tenant")
|
||||
errTooFewSchedulableNodes = errors.New("there is not enough schedulable nodes to satisfy this requirement")
|
||||
errFewerThanFourNodes = errors.New("at least 4 nodes are required for a tenant")
|
||||
)
|
||||
|
||||
// prepareError receives an error object and parse it against k8sErrors, returns the right error code paired with a generic error message
|
||||
func prepareError(err ...error) *models.Error {
|
||||
errorCode := int32(500)
|
||||
errorMessage := errorGeneric.Error()
|
||||
if len(err) > 0 {
|
||||
frame := getFrame(2)
|
||||
fileParts := strings.Split(frame.File, "/")
|
||||
LogError("original error -> (%s:%d: %v)", fileParts[len(fileParts)-1], frame.Line, err[0])
|
||||
if k8sErrors.IsUnauthorized(err[0]) {
|
||||
errorCode = 401
|
||||
errorMessage = errorGenericUnauthorized.Error()
|
||||
}
|
||||
if k8sErrors.IsForbidden(err[0]) {
|
||||
errorCode = 403
|
||||
errorMessage = errorGenericForbidden.Error()
|
||||
}
|
||||
if k8sErrors.IsNotFound(err[0]) {
|
||||
errorCode = 404
|
||||
errorMessage = ErrorGenericNotFound.Error()
|
||||
}
|
||||
if err[0] == ErrorGenericNotFound {
|
||||
errorCode = 404
|
||||
errorMessage = ErrorGenericNotFound.Error()
|
||||
}
|
||||
if errors.Is(err[0], errInvalidCredentials) {
|
||||
errorCode = 401
|
||||
errorMessage = errInvalidCredentials.Error()
|
||||
}
|
||||
// console invalid erasure coding value
|
||||
if errors.Is(err[0], errorInvalidErasureCodingValue) {
|
||||
errorCode = 400
|
||||
errorMessage = errorInvalidErasureCodingValue.Error()
|
||||
}
|
||||
if errors.Is(err[0], errBucketBodyNotInRequest) {
|
||||
errorCode = 400
|
||||
errorMessage = errBucketBodyNotInRequest.Error()
|
||||
}
|
||||
if errors.Is(err[0], errBucketNameNotInRequest) {
|
||||
errorCode = 400
|
||||
errorMessage = errBucketNameNotInRequest.Error()
|
||||
}
|
||||
if errors.Is(err[0], errGroupBodyNotInRequest) {
|
||||
errorCode = 400
|
||||
errorMessage = errGroupBodyNotInRequest.Error()
|
||||
}
|
||||
if errors.Is(err[0], errGroupNameNotInRequest) {
|
||||
errorCode = 400
|
||||
errorMessage = errGroupNameNotInRequest.Error()
|
||||
}
|
||||
if errors.Is(err[0], errPolicyNameNotInRequest) {
|
||||
errorCode = 400
|
||||
errorMessage = errPolicyNameNotInRequest.Error()
|
||||
}
|
||||
if errors.Is(err[0], errPolicyBodyNotInRequest) {
|
||||
errorCode = 400
|
||||
errorMessage = errPolicyBodyNotInRequest.Error()
|
||||
}
|
||||
// console invalid session error
|
||||
if errors.Is(err[0], errorGenericInvalidSession) {
|
||||
errorCode = 401
|
||||
errorMessage = errorGenericInvalidSession.Error()
|
||||
}
|
||||
// Bucket life cycle not configured
|
||||
if errors.Is(err[0], errBucketLifeCycleNotConfigured) {
|
||||
errorCode = 404
|
||||
errorMessage = errBucketLifeCycleNotConfigured.Error()
|
||||
}
|
||||
// Encryption not configured
|
||||
if errors.Is(err[0], errSSENotConfigured) {
|
||||
errorCode = 404
|
||||
errorMessage = errSSENotConfigured.Error()
|
||||
}
|
||||
// account change password
|
||||
if madmin.ToErrorResponse(err[0]).Code == "SignatureDoesNotMatch" {
|
||||
errorCode = 403
|
||||
errorMessage = errChangePassword.Error()
|
||||
}
|
||||
if errors.Is(err[0], errLicenseNotFound) {
|
||||
errorCode = 404
|
||||
errorMessage = errLicenseNotFound.Error()
|
||||
}
|
||||
if errors.Is(err[0], errInvalidLicense) {
|
||||
errorCode = 404
|
||||
errorMessage = errInvalidLicense.Error()
|
||||
}
|
||||
if errors.Is(err[0], errAvoidSelfAccountDelete) {
|
||||
errorCode = 403
|
||||
errorMessage = errAvoidSelfAccountDelete.Error()
|
||||
}
|
||||
if madmin.ToErrorResponse(err[0]).Code == "AccessDenied" {
|
||||
errorCode = 403
|
||||
errorMessage = errAccessDenied.Error()
|
||||
}
|
||||
if madmin.ToErrorResponse(err[0]).Code == "InvalidAccessKeyId" {
|
||||
errorCode = 401
|
||||
errorMessage = errorGenericInvalidSession.Error()
|
||||
}
|
||||
// console invalid session error
|
||||
if madmin.ToErrorResponse(err[0]).Code == "XMinioAdminNoSuchUser" {
|
||||
errorCode = 401
|
||||
errorMessage = errorGenericInvalidSession.Error()
|
||||
}
|
||||
// if we received a second error take that as friendly message but dont override the code
|
||||
if len(err) > 1 && err[1] != nil {
|
||||
LogError("friendly error: %v", err[1].Error())
|
||||
errorMessage = err[1].Error()
|
||||
}
|
||||
// if we receive third error we just print that as debugging
|
||||
if len(err) > 2 && err[2] != nil {
|
||||
LogError("debugging error: %v", err[2].Error())
|
||||
}
|
||||
|
||||
errRemoteTierExists := errors.New("Specified remote tier already exists") //nolint
|
||||
if errors.Is(err[0], errRemoteTierExists) {
|
||||
errorMessage = err[0].Error()
|
||||
}
|
||||
if errors.Is(err[0], errTooFewNodes) {
|
||||
errorCode = 507
|
||||
errorMessage = errTooFewNodes.Error()
|
||||
}
|
||||
if errors.Is(err[0], errTooFewSchedulableNodes) {
|
||||
errorCode = 507
|
||||
errorMessage = errTooFewSchedulableNodes.Error()
|
||||
}
|
||||
if errors.Is(err[0], errFewerThanFourNodes) {
|
||||
errorCode = 507
|
||||
errorMessage = errFewerThanFourNodes.Error()
|
||||
}
|
||||
}
|
||||
return &models.Error{Code: errorCode, Message: swag.String(errorMessage), DetailedMessage: swag.String(err[0].Error())}
|
||||
}
|
||||
|
||||
func getFrame(skipFrames int) runtime.Frame {
|
||||
// We need the frame at index skipFrames+2, since we never want runtime.Callers and getFrame
|
||||
targetFrameIndex := skipFrames + 2
|
||||
|
||||
// Set size to targetFrameIndex+2 to ensure we have room for one more caller than we need
|
||||
programCounters := make([]uintptr, targetFrameIndex+2)
|
||||
n := runtime.Callers(0, programCounters)
|
||||
|
||||
frame := runtime.Frame{Function: "unknown"}
|
||||
if n > 0 {
|
||||
frames := runtime.CallersFrames(programCounters[:n])
|
||||
for more, frameIndex := true, 0; more && frameIndex <= targetFrameIndex; frameIndex++ {
|
||||
var frameCandidate runtime.Frame
|
||||
frameCandidate, more = frames.Next()
|
||||
if frameIndex == targetFrameIndex {
|
||||
frame = frameCandidate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return frame
|
||||
}
|
||||
@@ -33,44 +33,45 @@ import (
|
||||
"github.com/minio/console/models"
|
||||
opauth "github.com/minio/console/operatorapi/auth"
|
||||
"github.com/minio/console/operatorapi/operations"
|
||||
"github.com/minio/console/operatorapi/operations/user_api"
|
||||
authApi "github.com/minio/console/operatorapi/operations/auth"
|
||||
|
||||
"github.com/minio/console/pkg/auth"
|
||||
"github.com/minio/console/pkg/auth/idp/oauth2"
|
||||
)
|
||||
|
||||
func registerLoginHandlers(api *operations.OperatorAPI) {
|
||||
// GET login strategy
|
||||
api.UserAPILoginDetailHandler = user_api.LoginDetailHandlerFunc(func(params user_api.LoginDetailParams) middleware.Responder {
|
||||
loginDetails, err := getLoginDetailsResponse(params.HTTPRequest)
|
||||
api.AuthLoginDetailHandler = authApi.LoginDetailHandlerFunc(func(params authApi.LoginDetailParams) middleware.Responder {
|
||||
loginDetails, err := getLoginDetailsResponse(params)
|
||||
if err != nil {
|
||||
return user_api.NewLoginDetailDefault(int(err.Code)).WithPayload(err)
|
||||
return authApi.NewLoginDetailDefault(int(err.Code)).WithPayload(err)
|
||||
}
|
||||
return user_api.NewLoginDetailOK().WithPayload(loginDetails)
|
||||
return authApi.NewLoginDetailOK().WithPayload(loginDetails)
|
||||
})
|
||||
// POST login using k8s service account token
|
||||
api.UserAPILoginOperatorHandler = user_api.LoginOperatorHandlerFunc(func(params user_api.LoginOperatorParams) middleware.Responder {
|
||||
loginResponse, err := getLoginOperatorResponse(params.Body)
|
||||
api.AuthLoginOperatorHandler = authApi.LoginOperatorHandlerFunc(func(params authApi.LoginOperatorParams) middleware.Responder {
|
||||
loginResponse, err := getLoginOperatorResponse(params)
|
||||
if err != nil {
|
||||
return user_api.NewLoginOperatorDefault(int(err.Code)).WithPayload(err)
|
||||
return authApi.NewLoginOperatorDefault(int(err.Code)).WithPayload(err)
|
||||
}
|
||||
// Custom response writer to set the session cookies
|
||||
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
|
||||
cookie := restapi.NewSessionCookieForConsole(loginResponse.SessionID)
|
||||
http.SetCookie(w, &cookie)
|
||||
user_api.NewLoginOperatorNoContent().WriteResponse(w, p)
|
||||
authApi.NewLoginOperatorNoContent().WriteResponse(w, p)
|
||||
})
|
||||
})
|
||||
// POST login using external IDP
|
||||
api.UserAPILoginOauth2AuthHandler = user_api.LoginOauth2AuthHandlerFunc(func(params user_api.LoginOauth2AuthParams) middleware.Responder {
|
||||
loginResponse, err := getLoginOauth2AuthResponse(params.HTTPRequest, params.Body)
|
||||
api.AuthLoginOauth2AuthHandler = authApi.LoginOauth2AuthHandlerFunc(func(params authApi.LoginOauth2AuthParams) middleware.Responder {
|
||||
loginResponse, err := getLoginOauth2AuthResponse(params)
|
||||
if err != nil {
|
||||
return user_api.NewLoginOauth2AuthDefault(int(err.Code)).WithPayload(err)
|
||||
return authApi.NewLoginOauth2AuthDefault(int(err.Code)).WithPayload(err)
|
||||
}
|
||||
// Custom response writer to set the session cookies
|
||||
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
|
||||
cookie := restapi.NewSessionCookieForConsole(loginResponse.SessionID)
|
||||
http.SetCookie(w, &cookie)
|
||||
user_api.NewLoginOauth2AuthNoContent().WriteResponse(w, p)
|
||||
authApi.NewLoginOauth2AuthNoContent().WriteResponse(w, p)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -86,14 +87,19 @@ func login(credentials restapi.ConsoleCredentialsI) (*string, error) {
|
||||
// if we made it here, the consoleCredentials work, generate a jwt with claims
|
||||
token, err := auth.NewEncryptedTokenForClient(&tokens, credentials.GetAccountAccessKey(), nil)
|
||||
if err != nil {
|
||||
LogError("error authenticating user: %v", err)
|
||||
return nil, errInvalidCredentials
|
||||
restapi.LogError("error authenticating user: %v", err)
|
||||
return nil, restapi.ErrInvalidLogin
|
||||
}
|
||||
return &token, nil
|
||||
}
|
||||
|
||||
// getLoginDetailsResponse returns information regarding the Console authentication mechanism.
|
||||
func getLoginDetailsResponse(r *http.Request) (*models.LoginDetails, *models.Error) {
|
||||
func getLoginDetailsResponse(params authApi.LoginDetailParams) (*models.LoginDetails, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
r := params.HTTPRequest
|
||||
|
||||
loginStrategy := models.LoginDetailsLoginStrategyServiceDashAccount
|
||||
redirectURL := ""
|
||||
|
||||
@@ -102,7 +108,7 @@ func getLoginDetailsResponse(r *http.Request) (*models.LoginDetails, *models.Err
|
||||
// initialize new oauth2 client
|
||||
oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, r, restapi.GetConsoleHTTPClient())
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
// Validate user against IDP
|
||||
identityProvider := &auth.IdentityProvider{Client: oauth2Client}
|
||||
@@ -125,31 +131,35 @@ func verifyUserAgainstIDP(ctx context.Context, provider auth.IdentityProviderI,
|
||||
return oauth2Token, nil
|
||||
}
|
||||
|
||||
func getLoginOauth2AuthResponse(r *http.Request, lr *models.LoginOauth2AuthRequest) (*models.LoginResponse, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
func getLoginOauth2AuthResponse(params authApi.LoginOauth2AuthParams) (*models.LoginResponse, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
r := params.HTTPRequest
|
||||
lr := params.Body
|
||||
|
||||
if oauth2.IsIDPEnabled() {
|
||||
// initialize new oauth2 client
|
||||
oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, r, restapi.GetConsoleHTTPClient())
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
// initialize new identity provider
|
||||
identityProvider := auth.IdentityProvider{Client: oauth2Client}
|
||||
// Validate user against IDP
|
||||
_, err = verifyUserAgainstIDP(ctx, identityProvider, *lr.Code, *lr.State)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
// If we pass here that means the IDP correctly authenticate the user with the operator resource
|
||||
// we proceed to use the service account token configured in the operator-console pod
|
||||
creds, err := newConsoleCredentials(getK8sSAToken())
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
token, err := login(restapi.ConsoleCredentials{ConsoleCredentials: creds})
|
||||
if err != nil {
|
||||
return nil, prepareError(errInvalidCredentials, nil, err)
|
||||
return nil, restapi.ErrorWithContext(ctx, restapi.ErrInvalidLogin, nil, err)
|
||||
}
|
||||
// serialize output
|
||||
loginResponse := &models.LoginResponse{
|
||||
@@ -157,7 +167,7 @@ func getLoginOauth2AuthResponse(r *http.Request, lr *models.LoginOauth2AuthReque
|
||||
}
|
||||
return loginResponse, nil
|
||||
}
|
||||
return nil, prepareError(errorGeneric)
|
||||
return nil, restapi.ErrorWithContext(ctx, restapi.ErrDefault)
|
||||
}
|
||||
|
||||
func newConsoleCredentials(secretKey string) (*credentials.Credentials, error) {
|
||||
@@ -169,17 +179,22 @@ func newConsoleCredentials(secretKey string) (*credentials.Credentials, error) {
|
||||
}
|
||||
|
||||
// getLoginOperatorResponse validate the provided service account token against k8s api
|
||||
func getLoginOperatorResponse(lmr *models.LoginOperatorRequest) (*models.LoginResponse, *models.Error) {
|
||||
func getLoginOperatorResponse(params authApi.LoginOperatorParams) (*models.LoginResponse, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
lmr := params.Body
|
||||
|
||||
creds, err := newConsoleCredentials(*lmr.Jwt)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
consoleCreds := restapi.ConsoleCredentials{ConsoleCredentials: creds}
|
||||
// Set a random as access key as session identifier
|
||||
consoleCreds.AccountAccessKey = fmt.Sprintf("%d", rand.Intn(100000-10000)+10000)
|
||||
token, err := login(consoleCreds)
|
||||
if err != nil {
|
||||
return nil, prepareError(errInvalidCredentials, nil, err)
|
||||
return nil, restapi.ErrorWithContext(ctx, restapi.ErrInvalidLogin, nil, err)
|
||||
}
|
||||
// serialize output
|
||||
loginResponse := &models.LoginResponse{
|
||||
|
||||
@@ -23,20 +23,20 @@ import (
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/console/operatorapi/operations"
|
||||
"github.com/minio/console/operatorapi/operations/user_api"
|
||||
authApi "github.com/minio/console/operatorapi/operations/auth"
|
||||
"github.com/minio/console/restapi"
|
||||
)
|
||||
|
||||
func registerLogoutHandlers(api *operations.OperatorAPI) {
|
||||
// logout from console
|
||||
api.UserAPILogoutHandler = user_api.LogoutHandlerFunc(func(params user_api.LogoutParams, session *models.Principal) middleware.Responder {
|
||||
api.AuthLogoutHandler = authApi.LogoutHandlerFunc(func(params authApi.LogoutParams, session *models.Principal) middleware.Responder {
|
||||
// Custom response writer to expire the session cookies
|
||||
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
|
||||
expiredCookie := restapi.ExpireSessionCookie()
|
||||
// this will tell the browser to clear the cookie and invalidate user session
|
||||
// additionally we are deleting the cookie from the client side
|
||||
http.SetCookie(w, &expiredCookie)
|
||||
user_api.NewLogoutOK().WriteResponse(w, p)
|
||||
authApi.NewLogoutOK().WriteResponse(w, p)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -20,12 +20,13 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/minio/console/operatorapi/operations/operator_api"
|
||||
xerrors "github.com/minio/console/restapi"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/cluster"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/console/operatorapi/operations"
|
||||
"github.com/minio/console/operatorapi/operations/operator_api"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
@@ -44,12 +45,12 @@ func registerNamespaceHandlers(api *operations.OperatorAPI) {
|
||||
}
|
||||
|
||||
func getNamespaceCreatedResponse(session *models.Principal, params operator_api.CreateNamespaceParams) *models.Error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
clientset, err := cluster.K8sClient(session.STSSessionToken)
|
||||
|
||||
if err != nil {
|
||||
return prepareError(err)
|
||||
return xerrors.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
namespace := *params.Body.Name
|
||||
@@ -57,7 +58,7 @@ func getNamespaceCreatedResponse(session *models.Principal, params operator_api.
|
||||
errCreation := getNamespaceCreated(ctx, clientset.CoreV1(), namespace)
|
||||
|
||||
if errCreation != nil {
|
||||
return prepareError(errCreation)
|
||||
return xerrors.ErrorWithContext(ctx, errCreation)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
"errors"
|
||||
"sort"
|
||||
|
||||
xerrors "github.com/minio/console/restapi"
|
||||
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
|
||||
"github.com/minio/console/operatorapi/operations/operator_api"
|
||||
@@ -52,8 +54,8 @@ func registerNodesHandlers(api *operations.OperatorAPI) {
|
||||
return operator_api.NewListNodeLabelsOK().WithPayload(*resp)
|
||||
})
|
||||
|
||||
api.OperatorAPIGetAllocatableResourcesHandler = operator_api.GetAllocatableResourcesHandlerFunc(func(params operator_api.GetAllocatableResourcesParams, principal *models.Principal) middleware.Responder {
|
||||
resp, err := getAllocatableResourcesResponse(params.NumNodes, principal)
|
||||
api.OperatorAPIGetAllocatableResourcesHandler = operator_api.GetAllocatableResourcesHandlerFunc(func(params operator_api.GetAllocatableResourcesParams, session *models.Principal) middleware.Responder {
|
||||
resp, err := getAllocatableResourcesResponse(session, params)
|
||||
if err != nil {
|
||||
return operator_api.NewGetAllocatableResourcesDefault(int(err.Code)).WithPayload(err)
|
||||
}
|
||||
@@ -71,7 +73,7 @@ type NodeResourceInfo struct {
|
||||
func getMaxAllocatableMemory(ctx context.Context, clientset v1.CoreV1Interface, numNodes int32) (*models.MaxAllocatableMemResponse, error) {
|
||||
// can't request less than 4 nodes
|
||||
if numNodes < 4 {
|
||||
return nil, errFewerThanFourNodes
|
||||
return nil, xerrors.ErrFewerThanFourNodes
|
||||
}
|
||||
|
||||
// get all nodes from cluster
|
||||
@@ -97,15 +99,15 @@ func getMaxAllocatableMemory(ctx context.Context, clientset v1.CoreV1Interface,
|
||||
}
|
||||
// requesting more nodes than schedulable and less than total number of workers
|
||||
if int(numNodes) > schedulableNodes && int(numNodes) < nonMasterNodes {
|
||||
return nil, errTooManyNodes
|
||||
return nil, xerrors.ErrTooManyNodes
|
||||
}
|
||||
if nonMasterNodes < int(numNodes) {
|
||||
return nil, errTooFewNodes
|
||||
return nil, xerrors.ErrTooFewNodes
|
||||
}
|
||||
|
||||
// not enough schedulable nodes
|
||||
if schedulableNodes < int(numNodes) {
|
||||
return nil, errTooFewSchedulableNodes
|
||||
return nil, xerrors.ErrTooFewAvailableNodes
|
||||
}
|
||||
|
||||
availableMemSizes := []int64{}
|
||||
@@ -177,12 +179,12 @@ func min(x, y int64) int64 {
|
||||
func getMaxAllocatableMemoryResponse(ctx context.Context, session *models.Principal, numNodes int32) (*models.MaxAllocatableMemResponse, *models.Error) {
|
||||
client, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, xerrors.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
clusterResources, err := getMaxAllocatableMemory(ctx, client.CoreV1(), numNodes)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, xerrors.ErrorWithContext(ctx, err)
|
||||
}
|
||||
return clusterResources, nil
|
||||
}
|
||||
@@ -217,12 +219,12 @@ func getNodeLabels(ctx context.Context, clientset v1.CoreV1Interface) (*models.N
|
||||
func getNodeLabelsResponse(ctx context.Context, session *models.Principal) (*models.NodeLabels, *models.Error) {
|
||||
client, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, xerrors.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
clusterResources, err := getNodeLabels(ctx, client.CoreV1())
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, xerrors.ErrorWithContext(ctx, err)
|
||||
}
|
||||
return clusterResources, nil
|
||||
}
|
||||
@@ -357,17 +359,16 @@ OUTER:
|
||||
|
||||
// Get allocatable resources response
|
||||
|
||||
func getAllocatableResourcesResponse(numNodes int32, session *models.Principal) (*models.AllocatableResourcesResponse, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
func getAllocatableResourcesResponse(session *models.Principal, params operator_api.GetAllocatableResourcesParams) (*models.AllocatableResourcesResponse, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
client, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, xerrors.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
clusterResources, err := getAllocatableResources(ctx, client.CoreV1(), numNodes)
|
||||
clusterResources, err := getAllocatableResources(ctx, client.CoreV1(), params.NumNodes)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, xerrors.ErrorWithContext(ctx, err)
|
||||
}
|
||||
return clusterResources, nil
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package auth
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the generate command
|
||||
@@ -46,7 +46,7 @@ func NewLoginDetail(ctx *middleware.Context, handler LoginDetailHandler) *LoginD
|
||||
return &LoginDetail{Context: ctx, Handler: handler}
|
||||
}
|
||||
|
||||
/* LoginDetail swagger:route GET /login UserAPI loginDetail
|
||||
/* LoginDetail swagger:route GET /login Auth loginDetail
|
||||
|
||||
Returns login strategy, form or sso.
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package auth
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package auth
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package auth
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the generate command
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package auth
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the generate command
|
||||
@@ -46,7 +46,7 @@ func NewLoginOauth2Auth(ctx *middleware.Context, handler LoginOauth2AuthHandler)
|
||||
return &LoginOauth2Auth{Context: ctx, Handler: handler}
|
||||
}
|
||||
|
||||
/* LoginOauth2Auth swagger:route POST /login/oauth2/auth UserAPI loginOauth2Auth
|
||||
/* LoginOauth2Auth swagger:route POST /login/oauth2/auth Auth loginOauth2Auth
|
||||
|
||||
Identity Provider oauth2 callback endpoint.
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package auth
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package auth
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package auth
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the generate command
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package auth
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the generate command
|
||||
@@ -46,7 +46,7 @@ func NewLoginOperator(ctx *middleware.Context, handler LoginOperatorHandler) *Lo
|
||||
return &LoginOperator{Context: ctx, Handler: handler}
|
||||
}
|
||||
|
||||
/* LoginOperator swagger:route POST /login/operator UserAPI loginOperator
|
||||
/* LoginOperator swagger:route POST /login/operator Auth loginOperator
|
||||
|
||||
Login to Operator Console.
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package auth
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package auth
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package auth
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the generate command
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package auth
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the generate command
|
||||
@@ -48,7 +48,7 @@ func NewLogout(ctx *middleware.Context, handler LogoutHandler) *Logout {
|
||||
return &Logout{Context: ctx, Handler: handler}
|
||||
}
|
||||
|
||||
/* Logout swagger:route POST /logout UserAPI logout
|
||||
/* Logout swagger:route POST /logout Auth logout
|
||||
|
||||
Logout from Operator.
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package auth
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package auth
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package auth
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the generate command
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package auth
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the generate command
|
||||
@@ -48,7 +48,7 @@ func NewSessionCheck(ctx *middleware.Context, handler SessionCheckHandler) *Sess
|
||||
return &SessionCheck{Context: ctx, Handler: handler}
|
||||
}
|
||||
|
||||
/* SessionCheck swagger:route GET /session UserAPI sessionCheck
|
||||
/* SessionCheck swagger:route GET /session Auth sessionCheck
|
||||
|
||||
Endpoint to check if your session is still valid
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package auth
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package auth
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
package auth
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the generate command
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
"github.com/go-openapi/swag"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/console/operatorapi/operations/auth"
|
||||
"github.com/minio/console/operatorapi/operations/operator_api"
|
||||
"github.com/minio/console/operatorapi/operations/user_api"
|
||||
)
|
||||
@@ -141,23 +142,23 @@ func NewOperatorAPI(spec *loads.Document) *OperatorAPI {
|
||||
OperatorAPIListTenantsHandler: operator_api.ListTenantsHandlerFunc(func(params operator_api.ListTenantsParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation operator_api.ListTenants has not yet been implemented")
|
||||
}),
|
||||
UserAPILoginDetailHandler: user_api.LoginDetailHandlerFunc(func(params user_api.LoginDetailParams) middleware.Responder {
|
||||
return middleware.NotImplemented("operation user_api.LoginDetail has not yet been implemented")
|
||||
AuthLoginDetailHandler: auth.LoginDetailHandlerFunc(func(params auth.LoginDetailParams) middleware.Responder {
|
||||
return middleware.NotImplemented("operation auth.LoginDetail has not yet been implemented")
|
||||
}),
|
||||
UserAPILoginOauth2AuthHandler: user_api.LoginOauth2AuthHandlerFunc(func(params user_api.LoginOauth2AuthParams) middleware.Responder {
|
||||
return middleware.NotImplemented("operation user_api.LoginOauth2Auth has not yet been implemented")
|
||||
AuthLoginOauth2AuthHandler: auth.LoginOauth2AuthHandlerFunc(func(params auth.LoginOauth2AuthParams) middleware.Responder {
|
||||
return middleware.NotImplemented("operation auth.LoginOauth2Auth has not yet been implemented")
|
||||
}),
|
||||
UserAPILoginOperatorHandler: user_api.LoginOperatorHandlerFunc(func(params user_api.LoginOperatorParams) middleware.Responder {
|
||||
return middleware.NotImplemented("operation user_api.LoginOperator has not yet been implemented")
|
||||
AuthLoginOperatorHandler: auth.LoginOperatorHandlerFunc(func(params auth.LoginOperatorParams) middleware.Responder {
|
||||
return middleware.NotImplemented("operation auth.LoginOperator has not yet been implemented")
|
||||
}),
|
||||
UserAPILogoutHandler: user_api.LogoutHandlerFunc(func(params user_api.LogoutParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation user_api.Logout has not yet been implemented")
|
||||
AuthLogoutHandler: auth.LogoutHandlerFunc(func(params auth.LogoutParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation auth.Logout has not yet been implemented")
|
||||
}),
|
||||
OperatorAPIPutTenantYAMLHandler: operator_api.PutTenantYAMLHandlerFunc(func(params operator_api.PutTenantYAMLParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation operator_api.PutTenantYAML has not yet been implemented")
|
||||
}),
|
||||
UserAPISessionCheckHandler: user_api.SessionCheckHandlerFunc(func(params user_api.SessionCheckParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation user_api.SessionCheck has not yet been implemented")
|
||||
AuthSessionCheckHandler: auth.SessionCheckHandlerFunc(func(params auth.SessionCheckParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation auth.SessionCheck has not yet been implemented")
|
||||
}),
|
||||
OperatorAPISetTenantLogsHandler: operator_api.SetTenantLogsHandlerFunc(func(params operator_api.SetTenantLogsParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation operator_api.SetTenantLogs has not yet been implemented")
|
||||
@@ -317,18 +318,18 @@ type OperatorAPI struct {
|
||||
OperatorAPIListPVCsForTenantHandler operator_api.ListPVCsForTenantHandler
|
||||
// OperatorAPIListTenantsHandler sets the operation handler for the list tenants operation
|
||||
OperatorAPIListTenantsHandler operator_api.ListTenantsHandler
|
||||
// UserAPILoginDetailHandler sets the operation handler for the login detail operation
|
||||
UserAPILoginDetailHandler user_api.LoginDetailHandler
|
||||
// UserAPILoginOauth2AuthHandler sets the operation handler for the login oauth2 auth operation
|
||||
UserAPILoginOauth2AuthHandler user_api.LoginOauth2AuthHandler
|
||||
// UserAPILoginOperatorHandler sets the operation handler for the login operator operation
|
||||
UserAPILoginOperatorHandler user_api.LoginOperatorHandler
|
||||
// UserAPILogoutHandler sets the operation handler for the logout operation
|
||||
UserAPILogoutHandler user_api.LogoutHandler
|
||||
// AuthLoginDetailHandler sets the operation handler for the login detail operation
|
||||
AuthLoginDetailHandler auth.LoginDetailHandler
|
||||
// AuthLoginOauth2AuthHandler sets the operation handler for the login oauth2 auth operation
|
||||
AuthLoginOauth2AuthHandler auth.LoginOauth2AuthHandler
|
||||
// AuthLoginOperatorHandler sets the operation handler for the login operator operation
|
||||
AuthLoginOperatorHandler auth.LoginOperatorHandler
|
||||
// AuthLogoutHandler sets the operation handler for the logout operation
|
||||
AuthLogoutHandler auth.LogoutHandler
|
||||
// OperatorAPIPutTenantYAMLHandler sets the operation handler for the put tenant y a m l operation
|
||||
OperatorAPIPutTenantYAMLHandler operator_api.PutTenantYAMLHandler
|
||||
// UserAPISessionCheckHandler sets the operation handler for the session check operation
|
||||
UserAPISessionCheckHandler user_api.SessionCheckHandler
|
||||
// AuthSessionCheckHandler sets the operation handler for the session check operation
|
||||
AuthSessionCheckHandler auth.SessionCheckHandler
|
||||
// OperatorAPISetTenantLogsHandler sets the operation handler for the set tenant logs operation
|
||||
OperatorAPISetTenantLogsHandler operator_api.SetTenantLogsHandler
|
||||
// OperatorAPISetTenantMonitoringHandler sets the operation handler for the set tenant monitoring operation
|
||||
@@ -526,23 +527,23 @@ func (o *OperatorAPI) Validate() error {
|
||||
if o.OperatorAPIListTenantsHandler == nil {
|
||||
unregistered = append(unregistered, "operator_api.ListTenantsHandler")
|
||||
}
|
||||
if o.UserAPILoginDetailHandler == nil {
|
||||
unregistered = append(unregistered, "user_api.LoginDetailHandler")
|
||||
if o.AuthLoginDetailHandler == nil {
|
||||
unregistered = append(unregistered, "auth.LoginDetailHandler")
|
||||
}
|
||||
if o.UserAPILoginOauth2AuthHandler == nil {
|
||||
unregistered = append(unregistered, "user_api.LoginOauth2AuthHandler")
|
||||
if o.AuthLoginOauth2AuthHandler == nil {
|
||||
unregistered = append(unregistered, "auth.LoginOauth2AuthHandler")
|
||||
}
|
||||
if o.UserAPILoginOperatorHandler == nil {
|
||||
unregistered = append(unregistered, "user_api.LoginOperatorHandler")
|
||||
if o.AuthLoginOperatorHandler == nil {
|
||||
unregistered = append(unregistered, "auth.LoginOperatorHandler")
|
||||
}
|
||||
if o.UserAPILogoutHandler == nil {
|
||||
unregistered = append(unregistered, "user_api.LogoutHandler")
|
||||
if o.AuthLogoutHandler == nil {
|
||||
unregistered = append(unregistered, "auth.LogoutHandler")
|
||||
}
|
||||
if o.OperatorAPIPutTenantYAMLHandler == nil {
|
||||
unregistered = append(unregistered, "operator_api.PutTenantYAMLHandler")
|
||||
}
|
||||
if o.UserAPISessionCheckHandler == nil {
|
||||
unregistered = append(unregistered, "user_api.SessionCheckHandler")
|
||||
if o.AuthSessionCheckHandler == nil {
|
||||
unregistered = append(unregistered, "auth.SessionCheckHandler")
|
||||
}
|
||||
if o.OperatorAPISetTenantLogsHandler == nil {
|
||||
unregistered = append(unregistered, "operator_api.SetTenantLogsHandler")
|
||||
@@ -806,19 +807,19 @@ func (o *OperatorAPI) initHandlerCache() {
|
||||
if o.handlers["GET"] == nil {
|
||||
o.handlers["GET"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["GET"]["/login"] = user_api.NewLoginDetail(o.context, o.UserAPILoginDetailHandler)
|
||||
o.handlers["GET"]["/login"] = auth.NewLoginDetail(o.context, o.AuthLoginDetailHandler)
|
||||
if o.handlers["POST"] == nil {
|
||||
o.handlers["POST"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["POST"]["/login/oauth2/auth"] = user_api.NewLoginOauth2Auth(o.context, o.UserAPILoginOauth2AuthHandler)
|
||||
o.handlers["POST"]["/login/oauth2/auth"] = auth.NewLoginOauth2Auth(o.context, o.AuthLoginOauth2AuthHandler)
|
||||
if o.handlers["POST"] == nil {
|
||||
o.handlers["POST"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["POST"]["/login/operator"] = user_api.NewLoginOperator(o.context, o.UserAPILoginOperatorHandler)
|
||||
o.handlers["POST"]["/login/operator"] = auth.NewLoginOperator(o.context, o.AuthLoginOperatorHandler)
|
||||
if o.handlers["POST"] == nil {
|
||||
o.handlers["POST"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["POST"]["/logout"] = user_api.NewLogout(o.context, o.UserAPILogoutHandler)
|
||||
o.handlers["POST"]["/logout"] = auth.NewLogout(o.context, o.AuthLogoutHandler)
|
||||
if o.handlers["PUT"] == nil {
|
||||
o.handlers["PUT"] = make(map[string]http.Handler)
|
||||
}
|
||||
@@ -826,7 +827,7 @@ func (o *OperatorAPI) initHandlerCache() {
|
||||
if o.handlers["GET"] == nil {
|
||||
o.handlers["GET"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["GET"]["/session"] = user_api.NewSessionCheck(o.context, o.UserAPISessionCheckHandler)
|
||||
o.handlers["GET"]["/session"] = auth.NewSessionCheck(o.context, o.AuthSessionCheckHandler)
|
||||
if o.handlers["PUT"] == nil {
|
||||
o.handlers["PUT"] = make(map[string]http.Handler)
|
||||
}
|
||||
|
||||
@@ -17,9 +17,10 @@
|
||||
package operatorapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/minio/console/restapi"
|
||||
errors "github.com/minio/console/restapi"
|
||||
|
||||
"github.com/minio/console/pkg/utils"
|
||||
|
||||
@@ -50,14 +51,14 @@ func GetParityInfo(nodes int64, disksPerNode int64) (models.ParityResponse, erro
|
||||
}
|
||||
|
||||
func getParityResponse(params operator_api.GetParityParams) (models.ParityResponse, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
nodes := params.Nodes
|
||||
disksPerNode := params.DisksPerNode
|
||||
|
||||
parityValues, err := GetParityInfo(nodes, disksPerNode)
|
||||
if err != nil {
|
||||
restapi.LogError("error getting parity info: %v", err)
|
||||
return nil, prepareError(err)
|
||||
errors.LogError("error getting parity info: %v", err)
|
||||
return nil, errors.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
return parityValues, nil
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
xerrors "github.com/minio/console/restapi"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
|
||||
"github.com/minio/console/cluster"
|
||||
@@ -94,18 +96,18 @@ func getResourceQuota(ctx context.Context, client K8sClientI, namespace, resourc
|
||||
}
|
||||
|
||||
func getResourceQuotaResponse(session *models.Principal, params operator_api.GetResourceQuotaParams) (*models.ResourceQuota, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
client, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, xerrors.ErrorWithContext(ctx, err)
|
||||
}
|
||||
k8sClient := &k8sClient{
|
||||
client: client,
|
||||
}
|
||||
resourceQuota, err := getResourceQuota(ctx, k8sClient, params.Namespace, params.ResourceQuotaName)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, xerrors.ErrorWithContext(ctx, err)
|
||||
}
|
||||
return resourceQuota, nil
|
||||
}
|
||||
|
||||
@@ -17,30 +17,35 @@
|
||||
package operatorapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
errors "github.com/minio/console/restapi"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/console/operatorapi/operations"
|
||||
"github.com/minio/console/operatorapi/operations/user_api"
|
||||
authApi "github.com/minio/console/operatorapi/operations/auth"
|
||||
)
|
||||
|
||||
func registerSessionHandlers(api *operations.OperatorAPI) {
|
||||
// session check
|
||||
api.UserAPISessionCheckHandler = user_api.SessionCheckHandlerFunc(func(params user_api.SessionCheckParams, session *models.Principal) middleware.Responder {
|
||||
sessionResp, err := getSessionResponse(session)
|
||||
api.AuthSessionCheckHandler = authApi.SessionCheckHandlerFunc(func(params authApi.SessionCheckParams, session *models.Principal) middleware.Responder {
|
||||
sessionResp, err := getSessionResponse(session, params)
|
||||
if err != nil {
|
||||
return user_api.NewSessionCheckDefault(int(err.Code)).WithPayload(err)
|
||||
return authApi.NewSessionCheckDefault(int(err.Code)).WithPayload(err)
|
||||
}
|
||||
return user_api.NewSessionCheckOK().WithPayload(sessionResp)
|
||||
return authApi.NewSessionCheckOK().WithPayload(sessionResp)
|
||||
})
|
||||
}
|
||||
|
||||
// getSessionResponse parse the token of the current session and returns a list of allowed actions to render in the UI
|
||||
func getSessionResponse(session *models.Principal) (*models.OperatorSessionResponse, *models.Error) {
|
||||
func getSessionResponse(session *models.Principal, params authApi.SessionCheckParams) (*models.OperatorSessionResponse, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
// serialize output
|
||||
if session == nil {
|
||||
return nil, prepareError(errorGenericInvalidSession)
|
||||
return nil, errors.ErrorWithContext(ctx, errors.ErrInvalidSession)
|
||||
}
|
||||
sessionResp := &models.OperatorSessionResponse{
|
||||
Status: models.OperatorSessionResponseStatusOk,
|
||||
|
||||
@@ -41,7 +41,7 @@ import (
|
||||
func getTenantCreatedResponse(session *models.Principal, params operator_api.CreateTenantParams) (response *models.CreateTenantResponse, mError *models.Error) {
|
||||
tenantReq := params.Body
|
||||
minioImage := tenantReq.Image
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
if minioImage == "" {
|
||||
minImg, err := cluster.GetMinioImage()
|
||||
@@ -56,7 +56,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
|
||||
client: clientSet,
|
||||
}
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
ns := *tenantReq.Namespace
|
||||
@@ -98,7 +98,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
|
||||
|
||||
_, err = clientSet.CoreV1().Secrets(ns).Create(ctx, &instanceSecret, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
// Enable/Disable console object browser for MinIO tenant (default is on)
|
||||
@@ -110,7 +110,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
|
||||
tenantConfigurationENV["MINIO_ROOT_USER"] = accessKey
|
||||
tenantConfigurationENV["MINIO_ROOT_PASSWORD"] = secretKey
|
||||
|
||||
// delete secrets created if an error occurred during tenant creation,
|
||||
// delete secrets created if an errors occurred during tenant creation,
|
||||
defer func() {
|
||||
if mError != nil {
|
||||
restapi.LogError("deleting secrets created for failed tenant: %s if any: %v", tenantName, mError)
|
||||
@@ -127,7 +127,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
|
||||
// Check the Erasure Coding Parity for validity and pass it to Tenant
|
||||
if tenantReq.ErasureCodingParity > 0 {
|
||||
if tenantReq.ErasureCodingParity < 2 || tenantReq.ErasureCodingParity > 8 {
|
||||
return nil, prepareError(errorInvalidErasureCodingValue)
|
||||
return nil, restapi.ErrorWithContext(ctx, restapi.ErrInvalidErasureCodingValue)
|
||||
}
|
||||
tenantConfigurationENV["MINIO_STORAGE_CLASS_STANDARD"] = fmt.Sprintf("EC:%d", tenantReq.ErasureCodingParity)
|
||||
}
|
||||
@@ -204,7 +204,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
|
||||
}
|
||||
_, err := clientSet.CoreV1().Secrets(ns).Create(ctx, &userSecret, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
}
|
||||
// attach the users to the tenant
|
||||
@@ -248,7 +248,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
|
||||
}
|
||||
_, err := clientSet.CoreV1().Secrets(ns).Create(ctx, &userSecret, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
}
|
||||
// attach the users to the tenant
|
||||
@@ -274,7 +274,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
|
||||
externalCertSecretName := fmt.Sprintf("%s-instance-external-certificates", secretName)
|
||||
externalCertSecret, err := createOrReplaceExternalCertSecrets(ctx, &k8sClient, ns, tenantReq.TLS.Minio, externalCertSecretName, tenantName)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
minInst.Spec.ExternalCertSecret = externalCertSecret
|
||||
}
|
||||
@@ -286,7 +286,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
|
||||
certificates := []*models.KeyPairConfiguration{tenantReq.Encryption.Client}
|
||||
certificateSecrets, err := createOrReplaceExternalCertSecrets(ctx, &k8sClient, ns, certificates, tenantExternalClientCertSecretName, tenantName)
|
||||
if err != nil {
|
||||
return nil, prepareError(restapi.ErrorGeneric)
|
||||
return nil, restapi.ErrorWithContext(ctx, restapi.ErrDefault)
|
||||
}
|
||||
if len(certificateSecrets) > 0 {
|
||||
minInst.Spec.ExternalClientCertSecret = certificateSecrets[0]
|
||||
@@ -296,7 +296,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
|
||||
// KES configuration for Tenant instance
|
||||
minInst.Spec.KES, err = getKESConfiguration(ctx, &k8sClient, ns, tenantReq.Encryption, secretName, tenantName)
|
||||
if err != nil {
|
||||
return nil, prepareError(restapi.ErrorGeneric)
|
||||
return nil, restapi.ErrorWithContext(ctx, restapi.ErrDefault)
|
||||
}
|
||||
// Set Labels, Annotations and Node Selector for KES
|
||||
minInst.Spec.KES.Labels = tenantReq.Encryption.Labels
|
||||
@@ -306,7 +306,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
|
||||
if tenantReq.Encryption.SecurityContext != nil {
|
||||
sc, err := convertModelSCToK8sSC(tenantReq.Encryption.SecurityContext)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
minInst.Spec.KES.SecurityContext = sc
|
||||
}
|
||||
@@ -317,7 +317,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
|
||||
for i, caCertificate := range tenantReq.TLS.CaCertificates {
|
||||
certificateContent, err := base64.StdEncoding.DecodeString(caCertificate)
|
||||
if err != nil {
|
||||
return nil, prepareError(restapi.ErrorGeneric, nil, err)
|
||||
return nil, restapi.ErrorWithContext(ctx, restapi.ErrDefault, nil, err)
|
||||
}
|
||||
caCertificates = append(caCertificates, tenantSecret{
|
||||
Name: fmt.Sprintf("ca-certificate-%d", i),
|
||||
@@ -329,7 +329,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
|
||||
if len(caCertificates) > 0 {
|
||||
certificateSecrets, err := createOrReplaceSecrets(ctx, &k8sClient, ns, caCertificates, tenantName)
|
||||
if err != nil {
|
||||
return nil, prepareError(restapi.ErrorGeneric, nil, err)
|
||||
return nil, restapi.ErrorWithContext(ctx, restapi.ErrDefault, nil, err)
|
||||
}
|
||||
minInst.Spec.ExternalCaCertSecret = certificateSecrets
|
||||
}
|
||||
@@ -347,7 +347,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
|
||||
pool, err := parseTenantPoolRequest(pool)
|
||||
if err != nil {
|
||||
restapi.LogError("parseTenantPoolRequest failed: %v", err)
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
minInst.Spec.Pools = append(minInst.Spec.Pools, *pool)
|
||||
}
|
||||
@@ -363,7 +363,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
|
||||
if tenantReq.ImagePullSecret != "" {
|
||||
imagePullSecret = tenantReq.ImagePullSecret
|
||||
} else if imagePullSecret, err = setImageRegistry(ctx, tenantReq.ImageRegistry, clientSet.CoreV1(), ns, tenantName); err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
// pass the image pull secret to the Tenant
|
||||
if imagePullSecret != "" {
|
||||
@@ -410,7 +410,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
|
||||
if tenantReq.LogSearchConfiguration.SecurityContext != nil {
|
||||
sc, err := convertModelSCToK8sSC(tenantReq.LogSearchConfiguration.SecurityContext)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
logSearchSecurityContext = sc
|
||||
}
|
||||
@@ -418,7 +418,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
|
||||
if tenantReq.LogSearchConfiguration.PostgresSecurityContext != nil {
|
||||
sc, err := convertModelSCToK8sSC(tenantReq.LogSearchConfiguration.PostgresSecurityContext)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
logSearchPgSecurityContext = sc
|
||||
}
|
||||
@@ -514,7 +514,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
|
||||
if tenantReq.PrometheusConfiguration != nil && tenantReq.PrometheusConfiguration.SecurityContext != nil {
|
||||
sc, err := convertModelSCToK8sSC(tenantReq.PrometheusConfiguration.SecurityContext)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
minInst.Spec.Prometheus.SecurityContext = sc
|
||||
}
|
||||
@@ -538,7 +538,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
|
||||
},
|
||||
}, tenantName)
|
||||
if err != nil {
|
||||
return nil, prepareError(restapi.ErrorGeneric, nil, err)
|
||||
return nil, restapi.ErrorWithContext(ctx, restapi.ErrDefault, nil, err)
|
||||
}
|
||||
minInst.Spec.Configuration = &corev1.LocalObjectReference{Name: tenantConfigurationName}
|
||||
|
||||
@@ -562,20 +562,20 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
|
||||
|
||||
opClient, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
_, err = opClient.MinioV2().Tenants(ns).Create(context.Background(), &minInst, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
restapi.LogError("Creating new tenant failed with: %v", err)
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
// Integrations
|
||||
if os.Getenv("GKE_INTEGRATION") != "" {
|
||||
err := gkeIntegration(clientSet, tenantName, ns, session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
}
|
||||
response = &models.CreateTenantResponse{
|
||||
|
||||
@@ -20,20 +20,21 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
errors "github.com/minio/console/restapi"
|
||||
|
||||
"github.com/minio/console/cluster"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/console/operatorapi/operations/operator_api"
|
||||
"github.com/minio/console/restapi"
|
||||
miniov2 "github.com/minio/operator/pkg/apis/minio.min.io/v2"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func getTenantDetailsResponse(session *models.Principal, params operator_api.TenantDetailsParams) (*models.Tenant, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, errors.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
opClient := &operatorClient{
|
||||
@@ -42,7 +43,7 @@ func getTenantDetailsResponse(session *models.Principal, params operator_api.Ten
|
||||
|
||||
minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, errors.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
info := getTenantInfo(minTenant)
|
||||
@@ -50,7 +51,7 @@ func getTenantDetailsResponse(session *models.Principal, params operator_api.Ten
|
||||
// get Kubernetes Client
|
||||
clientSet, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, errors.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
k8sClient := k8sClient{
|
||||
@@ -59,7 +60,7 @@ func getTenantDetailsResponse(session *models.Principal, params operator_api.Ten
|
||||
|
||||
tenantConfiguration, err := GetTenantConfiguration(ctx, &k8sClient, minTenant)
|
||||
if err != nil {
|
||||
restapi.LogError("unable to fetch configuration for tenant %s: %v", minTenant.Name, err)
|
||||
errors.LogError("unable to fetch configuration for tenant %s: %v", minTenant.Name, err)
|
||||
}
|
||||
|
||||
// detect if AD/LDAP is enabled
|
||||
@@ -105,14 +106,14 @@ func getTenantDetailsResponse(session *models.Principal, params operator_api.Ten
|
||||
//minio service
|
||||
minSvc, err := k8sClient.getService(ctx, minTenant.Namespace, minTenant.MinIOCIServiceName(), metav1.GetOptions{})
|
||||
if err != nil {
|
||||
// we can tolerate this error
|
||||
restapi.LogError("Unable to get MinIO service name: %v, continuing", err)
|
||||
// we can tolerate this errors
|
||||
errors.LogError("Unable to get MinIO service name: %v, continuing", err)
|
||||
}
|
||||
//console service
|
||||
conSvc, err := k8sClient.getService(ctx, minTenant.Namespace, minTenant.ConsoleCIServiceName(), metav1.GetOptions{})
|
||||
if err != nil {
|
||||
// we can tolerate this error
|
||||
restapi.LogError("Unable to get MinIO console service name: %v, continuing", err)
|
||||
// we can tolerate this errors
|
||||
errors.LogError("Unable to get MinIO console service name: %v, continuing", err)
|
||||
}
|
||||
|
||||
schema := "http"
|
||||
|
||||
@@ -22,9 +22,12 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/console/restapi"
|
||||
|
||||
"github.com/minio/console/pkg/http"
|
||||
|
||||
"github.com/minio/console/operatorapi/operations/operator_api"
|
||||
utils2 "github.com/minio/console/pkg/utils"
|
||||
"github.com/minio/console/restapi"
|
||||
miniov2 "github.com/minio/operator/pkg/apis/minio.min.io/v2"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
@@ -32,7 +35,7 @@ import (
|
||||
)
|
||||
|
||||
// updateTenantAction does an update on the minioTenant by patching the desired changes
|
||||
func updateTenantAction(ctx context.Context, operatorClient OperatorClientI, clientset v1.CoreV1Interface, httpCl utils2.HTTPClientI, namespace string, params operator_api.UpdateTenantParams) error {
|
||||
func updateTenantAction(ctx context.Context, operatorClient OperatorClientI, clientset v1.CoreV1Interface, httpCl http.ClientI, namespace string, params operator_api.UpdateTenantParams) error {
|
||||
imageToUpdate := params.Body.Image
|
||||
imageRegistryReq := params.Body.ImageRegistry
|
||||
|
||||
|
||||
@@ -33,11 +33,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
utils2 "github.com/minio/console/pkg/http"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/madmin-go"
|
||||
|
||||
utils2 "github.com/minio/console/pkg/utils"
|
||||
|
||||
"github.com/minio/console/restapi"
|
||||
|
||||
"github.com/minio/console/operatorapi/operations/operator_api"
|
||||
@@ -354,14 +354,16 @@ func registerTenantHandlers(api *operations.OperatorAPI) {
|
||||
|
||||
// getDeleteTenantResponse gets the output of deleting a minio instance
|
||||
func getDeleteTenantResponse(session *models.Principal, params operator_api.DeleteTenantParams) *models.Error {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err)
|
||||
return restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
// get Kubernetes Client
|
||||
clientset, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err)
|
||||
return restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
opClient := &operatorClient{
|
||||
client: opClientClientSet,
|
||||
@@ -373,12 +375,12 @@ func getDeleteTenantResponse(session *models.Principal, params operator_api.Dele
|
||||
|
||||
tenant, err := opClient.TenantGet(params.HTTPRequest.Context(), params.Namespace, params.Tenant, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return prepareError(err)
|
||||
return restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
tenant.EnsureDefaults()
|
||||
|
||||
if err = deleteTenantAction(params.HTTPRequest.Context(), opClient, clientset.CoreV1(), tenant, deleteTenantPVCs); err != nil {
|
||||
return prepareError(err)
|
||||
return restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -396,7 +398,7 @@ func deleteTenantAction(
|
||||
err := operatorClient.TenantDelete(ctx, tenant.Namespace, tenant.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
// try to delete pvc even if the tenant doesn't exist anymore but only if deletePvcs is set to true,
|
||||
// else, we return the error
|
||||
// else, we return the errors
|
||||
if (deletePvcs && !k8sErrors.IsNotFound(err)) || !deletePvcs {
|
||||
return err
|
||||
}
|
||||
@@ -440,19 +442,19 @@ func deleteTenantAction(
|
||||
|
||||
// getDeleteTenantResponse gets the output of deleting a minio instance
|
||||
func getDeletePodResponse(session *models.Principal, params operator_api.DeletePodParams) *models.Error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
// get Kubernetes Client
|
||||
clientset, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err)
|
||||
return restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
listOpts := metav1.ListOptions{
|
||||
LabelSelector: fmt.Sprintf("v1.min.io/tenant=%s", params.Tenant),
|
||||
FieldSelector: fmt.Sprintf("metadata.name=%s%s", params.Tenant, params.PodName[len(params.Tenant):]),
|
||||
}
|
||||
if err = clientset.CoreV1().Pods(params.Namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, listOpts); err != nil {
|
||||
return prepareError(err)
|
||||
return restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -494,12 +496,12 @@ func getTenantCreds(ctx context.Context, client K8sClientI, tenant *miniov2.Tena
|
||||
tenantAccessKey, ok := tenantConfiguration["accesskey"]
|
||||
if !ok {
|
||||
restapi.LogError("tenant's secret doesn't contain accesskey")
|
||||
return nil, restapi.ErrorGeneric
|
||||
return nil, restapi.ErrDefault
|
||||
}
|
||||
tenantSecretKey, ok := tenantConfiguration["secretkey"]
|
||||
if !ok {
|
||||
restapi.LogError("tenant's secret doesn't contain secretkey")
|
||||
return nil, restapi.ErrorGeneric
|
||||
return nil, restapi.ErrDefault
|
||||
}
|
||||
return &tenantKeys{accessKey: tenantAccessKey, secretKey: tenantSecretKey}, nil
|
||||
}
|
||||
@@ -584,6 +586,13 @@ func parseCertificate(name string, rawCert []byte) (*models.CertificateInfo, err
|
||||
}, nil
|
||||
}
|
||||
|
||||
var secretTypePublicKeyNameMap = map[string]string{
|
||||
"kubernetes.io/tls": "tls.crt",
|
||||
"cert-manager.io/v1": "tls.crt",
|
||||
"cert-manager.io/v1alpha2": "tls.crt",
|
||||
// Add newer secretTypes and their corresponding values in future
|
||||
}
|
||||
|
||||
// parseTenantCertificates convert public key pem certificates stored in k8s secrets for a given Tenant into x509 certificates
|
||||
func parseTenantCertificates(ctx context.Context, clientSet K8sClientI, namespace string, secrets []*miniov2.LocalCertificateReference) ([]*models.CertificateInfo, error) {
|
||||
var certificates []*models.CertificateInfo
|
||||
@@ -595,9 +604,11 @@ func parseTenantCertificates(ctx context.Context, clientSet K8sClientI, namespac
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if secret.Type == "kubernetes.io/tls" || secret.Type == "cert-manager.io/v1alpha2" {
|
||||
publicKey = "tls.crt"
|
||||
|
||||
if v, ok := secretTypePublicKeyNameMap[secret.Type]; ok {
|
||||
publicKey = v
|
||||
}
|
||||
|
||||
// Extract public key from certificate TLS secret
|
||||
if rawCert, ok := keyPair.Data[publicKey]; ok {
|
||||
var blocks []byte
|
||||
@@ -842,19 +853,19 @@ func updateTenantIdentityProvider(ctx context.Context, operatorClient OperatorCl
|
||||
|
||||
func getTenantIdentityProviderResponse(session *models.Principal, params operator_api.TenantIdentityProviderParams) (*models.IdpConfiguration, *models.Error) {
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
opClient := &operatorClient{
|
||||
client: opClientClientSet,
|
||||
}
|
||||
minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
// get Kubernetes Client
|
||||
clientSet, err := cluster.K8sClient(session.STSSessionToken)
|
||||
@@ -862,27 +873,27 @@ func getTenantIdentityProviderResponse(session *models.Principal, params operato
|
||||
client: clientSet,
|
||||
}
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
info, err := getTenantIdentityProvider(ctx, &k8sClient, minTenant)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func getUpdateTenantIdentityProviderResponse(session *models.Principal, params operator_api.UpdateTenantIdentityProviderParams) *models.Error {
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err)
|
||||
return restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
// get Kubernetes Client
|
||||
clientSet, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err)
|
||||
return restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
k8sClient := k8sClient{
|
||||
client: clientSet,
|
||||
@@ -891,25 +902,25 @@ func getUpdateTenantIdentityProviderResponse(session *models.Principal, params o
|
||||
client: opClientClientSet,
|
||||
}
|
||||
if err := updateTenantIdentityProvider(ctx, opClient, &k8sClient, params.Namespace, params); err != nil {
|
||||
return prepareError(err, errors.New("unable to update tenant"))
|
||||
return restapi.ErrorWithContext(ctx, err, errors.New("unable to update tenant"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTenantSecurityResponse(session *models.Principal, params operator_api.TenantSecurityParams) (*models.TenantSecurityResponse, *models.Error) {
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
opClient := &operatorClient{
|
||||
client: opClientClientSet,
|
||||
}
|
||||
minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
// get Kubernetes Client
|
||||
clientSet, err := cluster.K8sClient(session.STSSessionToken)
|
||||
@@ -917,27 +928,27 @@ func getTenantSecurityResponse(session *models.Principal, params operator_api.Te
|
||||
client: clientSet,
|
||||
}
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
info, err := getTenantSecurity(ctx, &k8sClient, minTenant)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func getUpdateTenantSecurityResponse(session *models.Principal, params operator_api.UpdateTenantSecurityParams) *models.Error {
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err)
|
||||
return restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
// get Kubernetes Client
|
||||
clientSet, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err)
|
||||
return restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
k8sClient := k8sClient{
|
||||
client: clientSet,
|
||||
@@ -946,7 +957,7 @@ func getUpdateTenantSecurityResponse(session *models.Principal, params operator_
|
||||
client: opClientClientSet,
|
||||
}
|
||||
if err := updateTenantSecurity(ctx, opClient, &k8sClient, params.Namespace, params); err != nil {
|
||||
return prepareError(err, errors.New("unable to update tenant"))
|
||||
return restapi.ErrorWithContext(ctx, err, errors.New("unable to update tenant"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1117,36 +1128,36 @@ func listTenants(ctx context.Context, operatorClient OperatorClientI, namespace
|
||||
}
|
||||
|
||||
func getListAllTenantsResponse(session *models.Principal, params operator_api.ListAllTenantsParams) (*models.ListTenantsResponse, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
opClient := &operatorClient{
|
||||
client: opClientClientSet,
|
||||
}
|
||||
listT, err := listTenants(ctx, opClient, "", params.Limit)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
return listT, nil
|
||||
}
|
||||
|
||||
// getListTenantsResponse list tenants by namespace
|
||||
func getListTenantsResponse(session *models.Principal, params operator_api.ListTenantsParams) (*models.ListTenantsResponse, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
opClient := &operatorClient{
|
||||
client: opClientClientSet,
|
||||
}
|
||||
listT, err := listTenants(ctx, opClient, params.Namespace, params.Limit)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
return listT, nil
|
||||
}
|
||||
@@ -1233,27 +1244,27 @@ func removeAnnotations(annotationsOne, annotationsTwo map[string]string) map[str
|
||||
}
|
||||
|
||||
func getUpdateTenantResponse(session *models.Principal, params operator_api.UpdateTenantParams) *models.Error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err)
|
||||
return restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
// get Kubernetes Client
|
||||
clientSet, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err)
|
||||
return restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
opClient := &operatorClient{
|
||||
client: opClientClientSet,
|
||||
}
|
||||
httpC := &utils2.HTTPClient{
|
||||
httpC := &utils2.Client{
|
||||
Client: &http.Client{
|
||||
Timeout: 4 * time.Second,
|
||||
},
|
||||
}
|
||||
if err := updateTenantAction(ctx, opClient, clientSet.CoreV1(), httpC, params.Namespace, params); err != nil {
|
||||
return prepareError(err, errors.New("unable to update tenant"))
|
||||
return restapi.ErrorWithContext(ctx, err, errors.New("unable to update tenant"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1284,17 +1295,17 @@ func addTenantPool(ctx context.Context, operatorClient OperatorClientI, params o
|
||||
}
|
||||
|
||||
func getTenantAddPoolResponse(session *models.Principal, params operator_api.TenantAddPoolParams) *models.Error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err)
|
||||
return restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
opClient := &operatorClient{
|
||||
client: opClientClientSet,
|
||||
}
|
||||
if err := addTenantPool(ctx, opClient, params); err != nil {
|
||||
return prepareError(err, errors.New("unable to add pool"))
|
||||
return restapi.ErrorWithContext(ctx, err, errors.New("unable to add pool"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1302,16 +1313,16 @@ func getTenantAddPoolResponse(session *models.Principal, params operator_api.Ten
|
||||
// getTenantUsageResponse returns the usage of a tenant
|
||||
func getTenantUsageResponse(session *models.Principal, params operator_api.GetTenantUsageParams) (*models.TenantUsage, *models.Error) {
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err, errorUnableToGetTenantUsage)
|
||||
return nil, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
|
||||
}
|
||||
clientSet, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err, errorUnableToGetTenantUsage)
|
||||
return nil, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
|
||||
}
|
||||
|
||||
opClient := &operatorClient{
|
||||
@@ -1323,7 +1334,7 @@ func getTenantUsageResponse(session *models.Principal, params operator_api.GetTe
|
||||
|
||||
minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant)
|
||||
if err != nil {
|
||||
return nil, prepareError(err, errorUnableToGetTenantUsage)
|
||||
return nil, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
|
||||
}
|
||||
minTenant.EnsureDefaults()
|
||||
|
||||
@@ -1336,7 +1347,7 @@ func getTenantUsageResponse(session *models.Principal, params operator_api.GetTe
|
||||
svcURL,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, prepareError(err, errorUnableToGetTenantUsage)
|
||||
return nil, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
@@ -1344,7 +1355,7 @@ func getTenantUsageResponse(session *models.Principal, params operator_api.GetTe
|
||||
// serialize output
|
||||
adminInfo, err := restapi.GetAdminInfo(ctx, adminClient)
|
||||
if err != nil {
|
||||
return nil, prepareError(err, errorUnableToGetTenantUsage)
|
||||
return nil, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
|
||||
}
|
||||
info := &models.TenantUsage{Used: adminInfo.Usage, DiskUsed: adminInfo.DisksUsage}
|
||||
return info, nil
|
||||
@@ -1353,12 +1364,12 @@ func getTenantUsageResponse(session *models.Principal, params operator_api.GetTe
|
||||
// getTenantLogsResponse returns the logs of a tenant
|
||||
func getTenantLogsResponse(session *models.Principal, params operator_api.GetTenantLogsParams) (*models.TenantLogs, *models.Error) {
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err, errorUnableToGetTenantLogs)
|
||||
return nil, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantLogs)
|
||||
}
|
||||
|
||||
opClient := &operatorClient{
|
||||
@@ -1367,7 +1378,7 @@ func getTenantLogsResponse(session *models.Principal, params operator_api.GetTen
|
||||
|
||||
minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant)
|
||||
if err != nil {
|
||||
return nil, prepareError(err, errorUnableToGetTenantLogs)
|
||||
return nil, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantLogs)
|
||||
}
|
||||
if minTenant.Spec.Log == nil {
|
||||
retval := &models.TenantLogs{
|
||||
@@ -1450,12 +1461,12 @@ func getTenantLogsResponse(session *models.Principal, params operator_api.GetTen
|
||||
// setTenantLogsResponse returns the logs of a tenant
|
||||
func setTenantLogsResponse(session *models.Principal, params operator_api.SetTenantLogsParams) (bool, *models.Error) {
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return false, prepareError(err, errorUnableToGetTenantUsage)
|
||||
return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
|
||||
}
|
||||
|
||||
opClient := &operatorClient{
|
||||
@@ -1464,7 +1475,7 @@ func setTenantLogsResponse(session *models.Principal, params operator_api.SetTen
|
||||
|
||||
minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant)
|
||||
if err != nil {
|
||||
return false, prepareError(err, errorUnableToGetTenantUsage)
|
||||
return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
|
||||
}
|
||||
|
||||
var labels = make(map[string]string)
|
||||
@@ -1493,7 +1504,7 @@ func setTenantLogsResponse(session *models.Principal, params operator_api.SetTen
|
||||
if reflect.TypeOf(params.Data.LogCPURequest).Kind() == reflect.String && params.Data.LogCPURequest != "0Gi" && params.Data.LogCPURequest != "" {
|
||||
cpuQuantity, err := resource.ParseQuantity(params.Data.LogCPURequest)
|
||||
if err != nil {
|
||||
return false, prepareError(err)
|
||||
return false, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
logResourceRequest["cpu"] = cpuQuantity
|
||||
minTenant.Spec.Log.Resources.Requests = logResourceRequest
|
||||
@@ -1501,7 +1512,7 @@ func setTenantLogsResponse(session *models.Principal, params operator_api.SetTen
|
||||
if reflect.TypeOf(params.Data.LogMemRequest).Kind() == reflect.String {
|
||||
memQuantity, err := resource.ParseQuantity(params.Data.LogMemRequest)
|
||||
if err != nil {
|
||||
return false, prepareError(err)
|
||||
return false, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
logResourceRequest["memory"] = memQuantity
|
||||
@@ -1538,7 +1549,7 @@ func setTenantLogsResponse(session *models.Principal, params operator_api.SetTen
|
||||
if reflect.TypeOf(params.Data.LogDBCPURequest).Kind() == reflect.String && params.Data.LogDBCPURequest != "0Gi" && params.Data.LogDBCPURequest != "" {
|
||||
dbCPUQuantity, err := resource.ParseQuantity(params.Data.LogDBCPURequest)
|
||||
if err != nil {
|
||||
return false, prepareError(err)
|
||||
return false, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
logDBResourceRequest["cpu"] = dbCPUQuantity
|
||||
minTenant.Spec.Log.Db.Resources.Requests = logDBResourceRequest
|
||||
@@ -1546,7 +1557,7 @@ func setTenantLogsResponse(session *models.Principal, params operator_api.SetTen
|
||||
if reflect.TypeOf(params.Data.LogDBMemRequest).Kind() == reflect.String {
|
||||
dbMemQuantity, err := resource.ParseQuantity(params.Data.LogDBMemRequest)
|
||||
if err != nil {
|
||||
return false, prepareError(err)
|
||||
return false, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
logDBResourceRequest["memory"] = dbMemQuantity
|
||||
minTenant.Spec.Log.Db.Resources.Requests = logDBResourceRequest
|
||||
@@ -1609,7 +1620,7 @@ func setTenantLogsResponse(session *models.Principal, params operator_api.SetTen
|
||||
|
||||
_, err = opClient.TenantUpdate(ctx, minTenant, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return false, prepareError(err)
|
||||
return false, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
@@ -1617,12 +1628,12 @@ func setTenantLogsResponse(session *models.Principal, params operator_api.SetTen
|
||||
// enableTenantLoggingResponse enables Tenant Logging
|
||||
func enableTenantLoggingResponse(session *models.Principal, params operator_api.EnableTenantLoggingParams) (bool, *models.Error) {
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return false, prepareError(err, errorUnableToGetTenantUsage)
|
||||
return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
|
||||
}
|
||||
|
||||
opClient := &operatorClient{
|
||||
@@ -1631,7 +1642,7 @@ func enableTenantLoggingResponse(session *models.Principal, params operator_api.
|
||||
|
||||
minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant)
|
||||
if err != nil {
|
||||
return false, prepareError(err, errorUnableToGetTenantUsage)
|
||||
return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
|
||||
}
|
||||
minTenant.EnsureDefaults()
|
||||
|
||||
@@ -1670,7 +1681,7 @@ func enableTenantLoggingResponse(session *models.Principal, params operator_api.
|
||||
|
||||
_, err = opClient.TenantUpdate(ctx, minTenant, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return false, prepareError(err)
|
||||
return false, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
@@ -1678,15 +1689,15 @@ func enableTenantLoggingResponse(session *models.Principal, params operator_api.
|
||||
// disableTenantLoggingResponse disables Tenant Logging
|
||||
func disableTenantLoggingResponse(session *models.Principal, params operator_api.DisableTenantLoggingParams) (bool, *models.Error) {
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return false, prepareError(err, errorUnableToGetTenantUsage)
|
||||
return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
|
||||
}
|
||||
if err != nil {
|
||||
return false, prepareError(err, errorUnableToGetTenantUsage)
|
||||
return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
|
||||
}
|
||||
|
||||
opClient := &operatorClient{
|
||||
@@ -1695,31 +1706,31 @@ func disableTenantLoggingResponse(session *models.Principal, params operator_api
|
||||
|
||||
minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant)
|
||||
if err != nil {
|
||||
return false, prepareError(err, errorUnableToGetTenantUsage)
|
||||
return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
|
||||
}
|
||||
minTenant.EnsureDefaults()
|
||||
minTenant.Spec.Log = nil
|
||||
|
||||
_, err = opClient.TenantUpdate(ctx, minTenant, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return false, prepareError(err)
|
||||
return false, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func getTenantPodsResponse(session *models.Principal, params operator_api.GetTenantPodsParams) ([]*models.TenantPod, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
clientset, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
listOpts := metav1.ListOptions{
|
||||
LabelSelector: fmt.Sprintf("%s=%s", miniov2.TenantLabel, params.Tenant),
|
||||
}
|
||||
pods, err := clientset.CoreV1().Pods(params.Namespace).List(ctx, listOpts)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
retval := []*models.TenantPod{}
|
||||
for _, pod := range pods.Items {
|
||||
@@ -1743,35 +1754,35 @@ func getTenantPodsResponse(session *models.Principal, params operator_api.GetTen
|
||||
}
|
||||
|
||||
func getPodLogsResponse(session *models.Principal, params operator_api.GetPodLogsParams) (string, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
clientset, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return "", prepareError(err)
|
||||
return "", restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
listOpts := &corev1.PodLogOptions{}
|
||||
logs := clientset.CoreV1().Pods(params.Namespace).GetLogs(params.PodName, listOpts)
|
||||
buff, err := logs.DoRaw(ctx)
|
||||
if err != nil {
|
||||
return "", prepareError(err)
|
||||
return "", restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
return string(buff), nil
|
||||
}
|
||||
|
||||
func getPodEventsResponse(session *models.Principal, params operator_api.GetPodEventsParams) (models.EventListWrapper, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
clientset, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
pod, err := clientset.CoreV1().Pods(params.Namespace).Get(ctx, params.PodName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
events, err := clientset.CoreV1().Events(params.Namespace).List(ctx, metav1.ListOptions{FieldSelector: fmt.Sprintf("involvedObject.uid=%s", pod.UID)})
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
retval := models.EventListWrapper{}
|
||||
for i := 0; i < len(events.Items); i++ {
|
||||
@@ -1791,12 +1802,12 @@ func getPodEventsResponse(session *models.Principal, params operator_api.GetPodE
|
||||
|
||||
//get values for prometheus metrics
|
||||
func getTenantMonitoringResponse(session *models.Principal, params operator_api.GetTenantMonitoringParams) (*models.TenantMonitoringInfo, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
opClient := &operatorClient{
|
||||
@@ -1805,7 +1816,7 @@ func getTenantMonitoringResponse(session *models.Principal, params operator_api.
|
||||
|
||||
minInst, err := opClient.TenantGet(ctx, params.Namespace, params.Tenant, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
monitoringInfo := &models.TenantMonitoringInfo{}
|
||||
@@ -1886,12 +1897,12 @@ func getTenantMonitoringResponse(session *models.Principal, params operator_api.
|
||||
//sets tenant Prometheus monitoring cofiguration fields to values provided
|
||||
func setTenantMonitoringResponse(session *models.Principal, params operator_api.SetTenantMonitoringParams) (bool, *models.Error) {
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return false, prepareError(err, errorUnableToGetTenantUsage)
|
||||
return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
|
||||
}
|
||||
|
||||
opClient := &operatorClient{
|
||||
@@ -1900,7 +1911,7 @@ func setTenantMonitoringResponse(session *models.Principal, params operator_api.
|
||||
|
||||
minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant)
|
||||
if err != nil {
|
||||
return false, prepareError(err, errorUnableToGetTenantUsage)
|
||||
return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage)
|
||||
}
|
||||
|
||||
if params.Data.Toggle {
|
||||
@@ -1916,7 +1927,7 @@ func setTenantMonitoringResponse(session *models.Principal, params operator_api.
|
||||
}
|
||||
_, err = opClient.TenantUpdate(ctx, minTenant, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return false, prepareError(err)
|
||||
return false, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
@@ -1944,7 +1955,7 @@ func setTenantMonitoringResponse(session *models.Principal, params operator_api.
|
||||
if params.Data.MonitoringCPURequest != "" {
|
||||
cpuQuantity, err := resource.ParseQuantity(params.Data.MonitoringCPURequest)
|
||||
if err != nil {
|
||||
return false, prepareError(err)
|
||||
return false, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
monitoringResourceRequest["cpu"] = cpuQuantity
|
||||
}
|
||||
@@ -1952,7 +1963,7 @@ func setTenantMonitoringResponse(session *models.Principal, params operator_api.
|
||||
if params.Data.MonitoringMemRequest != "" {
|
||||
memQuantity, err := resource.ParseQuantity(params.Data.MonitoringMemRequest)
|
||||
if err != nil {
|
||||
return false, prepareError(err)
|
||||
return false, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
monitoringResourceRequest["memory"] = memQuantity
|
||||
}
|
||||
@@ -1977,7 +1988,7 @@ func setTenantMonitoringResponse(session *models.Principal, params operator_api.
|
||||
minTenant.Spec.Prometheus.ServiceAccountName = params.Data.ServiceAccountName
|
||||
_, err = opClient.TenantUpdate(ctx, minTenant, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return false, prepareError(err)
|
||||
return false, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
@@ -2422,11 +2433,11 @@ func parseNodeSelectorTerm(term *corev1.NodeSelectorTerm) *models.NodeSelectorTe
|
||||
}
|
||||
|
||||
func getTenantUpdatePoolResponse(session *models.Principal, params operator_api.TenantUpdatePoolsParams) (*models.Tenant, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
opClient := &operatorClient{
|
||||
@@ -2436,7 +2447,7 @@ func getTenantUpdatePoolResponse(session *models.Principal, params operator_api.
|
||||
t, err := updateTenantPools(ctx, opClient, params.Namespace, params.Tenant, params.Body.Pools)
|
||||
if err != nil {
|
||||
restapi.LogError("error updating Tenant's pools: %v", err)
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
// parse it to models.Tenant
|
||||
@@ -2487,20 +2498,19 @@ func updateTenantPools(
|
||||
}
|
||||
|
||||
func getTenantYAML(session *models.Principal, params operator_api.GetTenantYAMLParams) (*models.TenantYAML, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
// get Kubernetes Client
|
||||
|
||||
opClient, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
tenant, err := opClient.MinioV2().Tenants(params.Namespace).Get(params.HTTPRequest.Context(), params.Tenant, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
// remove managed fields
|
||||
tenant.ManagedFields = []metav1.ManagedFieldsEntry{}
|
||||
|
||||
//yb, err := yaml.Marshal(tenant)
|
||||
j8sJSONSerializer := k8sJson.NewSerializerWithOptions(
|
||||
k8sJson.DefaultMetaFactory, nil, nil,
|
||||
@@ -2514,7 +2524,7 @@ func getTenantYAML(session *models.Principal, params operator_api.GetTenantYAMLP
|
||||
|
||||
err = j8sJSONSerializer.Encode(tenant, buf)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
yb := buf.String()
|
||||
@@ -2523,6 +2533,8 @@ func getTenantYAML(session *models.Principal, params operator_api.GetTenantYAMLP
|
||||
}
|
||||
|
||||
func getUpdateTenantYAML(session *models.Principal, params operator_api.PutTenantYAMLParams) *models.Error {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
// https://godoc.org/k8s.io/apimachinery/pkg/runtime#Scheme
|
||||
scheme := runtime.NewScheme()
|
||||
|
||||
@@ -2540,12 +2552,12 @@ func getUpdateTenantYAML(session *models.Principal, params operator_api.PutTenan
|
||||
// get Kubernetes Client
|
||||
opClient, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err)
|
||||
return restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
tenant, err := opClient.MinioV2().Tenants(params.Namespace).Get(params.HTTPRequest.Context(), params.Tenant, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return prepareError(err)
|
||||
return restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
upTenant := tenant.DeepCopy()
|
||||
// only update safe fields: spec, metadata.finalizers, metadata.labels and metadata.annotations
|
||||
@@ -2563,23 +2575,23 @@ func getUpdateTenantYAML(session *models.Principal, params operator_api.PutTenan
|
||||
}
|
||||
|
||||
func getTenantEventsResponse(session *models.Principal, params operator_api.GetTenantEventsParams) (models.EventListWrapper, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
client, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
clientset, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
tenant, err := client.MinioV2().Tenants(params.Namespace).Get(ctx, params.Tenant, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
events, err := clientset.CoreV1().Events(params.Namespace).List(ctx, metav1.ListOptions{FieldSelector: fmt.Sprintf("involvedObject.uid=%s", tenant.UID)})
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
retval := models.EventListWrapper{}
|
||||
for _, event := range events.Items {
|
||||
@@ -2603,7 +2615,7 @@ func getUpdateDomainsResponse(session *models.Principal, params operator_api.Upd
|
||||
|
||||
operatorCli, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err)
|
||||
return restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
opClient := &operatorClient{
|
||||
@@ -2613,7 +2625,7 @@ func getUpdateDomainsResponse(session *models.Principal, params operator_api.Upd
|
||||
err = updateTenantDomains(ctx, opClient, params.Namespace, params.Tenant, params.Body.Domains)
|
||||
|
||||
if err != nil {
|
||||
return prepareError(err)
|
||||
return restapi.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -27,6 +27,8 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
xerrors "github.com/minio/console/restapi"
|
||||
|
||||
"github.com/minio/console/operatorapi/operations/operator_api"
|
||||
|
||||
"errors"
|
||||
@@ -108,25 +110,25 @@ func tenantUpdateCertificates(ctx context.Context, operatorClient OperatorClient
|
||||
|
||||
// getTenantUpdateCertificatesResponse wrapper of tenantUpdateCertificates
|
||||
func getTenantUpdateCertificatesResponse(session *models.Principal, params operator_api.TenantUpdateCertificateParams) *models.Error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
// get Kubernetes Client
|
||||
clientSet, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err, errorUnableToUpdateTenantCertificates)
|
||||
return xerrors.ErrorWithContext(ctx, err, xerrors.ErrUnableToUpdateTenantCertificates)
|
||||
}
|
||||
k8sClient := k8sClient{
|
||||
client: clientSet,
|
||||
}
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err, errorUnableToUpdateTenantCertificates)
|
||||
return xerrors.ErrorWithContext(ctx, err, xerrors.ErrUnableToUpdateTenantCertificates)
|
||||
}
|
||||
opClient := operatorClient{
|
||||
client: opClientClientSet,
|
||||
}
|
||||
if err := tenantUpdateCertificates(ctx, &opClient, &k8sClient, params.Namespace, params); err != nil {
|
||||
return prepareError(err, errorUnableToUpdateTenantCertificates)
|
||||
return xerrors.ErrorWithContext(ctx, err, xerrors.ErrUnableToUpdateTenantCertificates)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -239,42 +241,42 @@ func tenantUpdateEncryption(ctx context.Context, operatorClient OperatorClientI,
|
||||
|
||||
// getTenantDeleteEncryptionResponse is a wrapper for tenantDeleteEncryption
|
||||
func getTenantDeleteEncryptionResponse(session *models.Principal, params operator_api.TenantDeleteEncryptionParams) *models.Error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err, errorDeletingEncryptionConfig)
|
||||
return xerrors.ErrorWithContext(ctx, err, xerrors.ErrDeletingEncryptionConfig)
|
||||
}
|
||||
opClient := operatorClient{
|
||||
client: opClientClientSet,
|
||||
}
|
||||
if err := tenantDeleteEncryption(ctx, &opClient, params.Namespace, params); err != nil {
|
||||
return prepareError(err, errorDeletingEncryptionConfig)
|
||||
return xerrors.ErrorWithContext(ctx, err, xerrors.ErrDeletingEncryptionConfig)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTenantUpdateEncryptionResponse is a wrapper for tenantUpdateEncryption
|
||||
func getTenantUpdateEncryptionResponse(session *models.Principal, params operator_api.TenantUpdateEncryptionParams) *models.Error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
// get Kubernetes Client
|
||||
clientSet, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err, errorUpdatingEncryptionConfig)
|
||||
return xerrors.ErrorWithContext(ctx, err, xerrors.ErrUpdatingEncryptionConfig)
|
||||
}
|
||||
k8sClient := k8sClient{
|
||||
client: clientSet,
|
||||
}
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err, errorUpdatingEncryptionConfig)
|
||||
return xerrors.ErrorWithContext(ctx, err, xerrors.ErrUpdatingEncryptionConfig)
|
||||
}
|
||||
opClient := operatorClient{
|
||||
client: opClientClientSet,
|
||||
}
|
||||
if err := tenantUpdateEncryption(ctx, &opClient, &k8sClient, params.Namespace, params); err != nil {
|
||||
return prepareError(err, errorUpdatingEncryptionConfig)
|
||||
return xerrors.ErrorWithContext(ctx, err, xerrors.ErrUpdatingEncryptionConfig)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -453,26 +455,26 @@ func tenantEncryptionInfo(ctx context.Context, operatorClient OperatorClientI, c
|
||||
|
||||
// getTenantEncryptionResponse is a wrapper for tenantEncryptionInfo
|
||||
func getTenantEncryptionInfoResponse(session *models.Principal, params operator_api.TenantEncryptionInfoParams) (*models.EncryptionConfigurationResponse, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
// get Kubernetes Client
|
||||
clientSet, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err, errorEncryptionConfigNotFound)
|
||||
return nil, xerrors.ErrorWithContext(ctx, err, xerrors.ErrEncryptionConfigNotFound)
|
||||
}
|
||||
k8sClient := k8sClient{
|
||||
client: clientSet,
|
||||
}
|
||||
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err, errorEncryptionConfigNotFound)
|
||||
return nil, xerrors.ErrorWithContext(ctx, err, xerrors.ErrEncryptionConfigNotFound)
|
||||
}
|
||||
opClient := operatorClient{
|
||||
client: opClientClientSet,
|
||||
}
|
||||
configuration, err := tenantEncryptionInfo(ctx, &opClient, &k8sClient, params.Namespace, params)
|
||||
if err != nil {
|
||||
return nil, prepareError(err, errorEncryptionConfigNotFound)
|
||||
return nil, xerrors.ErrorWithContext(ctx, err, xerrors.ErrEncryptionConfigNotFound)
|
||||
}
|
||||
return configuration, nil
|
||||
}
|
||||
@@ -541,8 +543,8 @@ func createOrReplaceSecrets(ctx context.Context, clientSet K8sClientI, ns string
|
||||
// delete secret with same name if exists
|
||||
err := clientSet.deleteSecret(ctx, ns, secret.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
// log the error if any and continue
|
||||
LogError("deleting secret name %s failed: %v, continuing..", secret.Name, err)
|
||||
// log the errors if any and continue
|
||||
xerrors.LogError("deleting secret name %s failed: %v, continuing..", secret.Name, err)
|
||||
}
|
||||
k8sSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -579,8 +581,8 @@ func createOrReplaceExternalCertSecrets(ctx context.Context, clientSet K8sClient
|
||||
// delete secret with same name if exists
|
||||
err := clientSet.deleteSecret(ctx, ns, keyPairSecretName, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
// log the error if any and continue
|
||||
LogError("deleting secret name %s failed: %v, continuing..", keyPairSecretName, err)
|
||||
// log the errors if any and continue
|
||||
xerrors.LogError("deleting secret name %s failed: %v, continuing..", keyPairSecretName, err)
|
||||
}
|
||||
imm := true
|
||||
tlsCrt, err := base64.StdEncoding.DecodeString(*keyPair.Crt)
|
||||
@@ -625,13 +627,13 @@ func createOrReplaceExternalCertSecrets(ctx context.Context, clientSet K8sClient
|
||||
func createOrReplaceKesConfigurationSecrets(ctx context.Context, clientSet K8sClientI, ns string, encryptionCfg *models.EncryptionConfiguration, kesConfigurationSecretName, kesClientCertSecretName, tenantName string) (*corev1.LocalObjectReference, *miniov2.LocalCertificateReference, error) {
|
||||
// delete KES configuration secret if exists
|
||||
if err := clientSet.deleteSecret(ctx, ns, kesConfigurationSecretName, metav1.DeleteOptions{}); err != nil {
|
||||
// log the error if any and continue
|
||||
LogError("deleting secret name %s failed: %v, continuing..", kesConfigurationSecretName, err)
|
||||
// log the errors if any and continue
|
||||
xerrors.LogError("deleting secret name %s failed: %v, continuing..", kesConfigurationSecretName, err)
|
||||
}
|
||||
// delete KES client cert secret if exists
|
||||
if err := clientSet.deleteSecret(ctx, ns, kesClientCertSecretName, metav1.DeleteOptions{}); err != nil {
|
||||
// log the error if any and continue
|
||||
LogError("deleting secret name %s failed: %v, continuing..", kesClientCertSecretName, err)
|
||||
// log the errors if any and continue
|
||||
xerrors.LogError("deleting secret name %s failed: %v, continuing..", kesClientCertSecretName, err)
|
||||
}
|
||||
// if autoCert is enabled then Operator will generate the client certificates, calculate the client cert identity
|
||||
// and pass it to KES via the ${MINIO_KES_IDENTITY} variable
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/console/pkg/utils"
|
||||
xhttp "github.com/minio/console/pkg/http"
|
||||
|
||||
"github.com/minio/console/operatorapi/operations/operator_api"
|
||||
|
||||
@@ -897,7 +897,7 @@ func Test_UpdateTenantAction(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
operatorClient OperatorClientI
|
||||
httpCl utils.HTTPClientI
|
||||
httpCl xhttp.ClientI
|
||||
nameSpace string
|
||||
tenantName string
|
||||
mockTenantPatch func(ctx context.Context, namespace string, tenantName string, pt types.PatchType, data []byte, options metav1.PatchOptions) (*miniov2.Tenant, error)
|
||||
|
||||
@@ -17,20 +17,24 @@
|
||||
package operatorapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
errors "github.com/minio/console/restapi"
|
||||
|
||||
xhttp "github.com/minio/console/pkg/http"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/console/operatorapi/operations"
|
||||
"github.com/minio/console/operatorapi/operations/user_api"
|
||||
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/minio/console/pkg/utils"
|
||||
)
|
||||
|
||||
func registerVersionHandlers(api *operations.OperatorAPI) {
|
||||
api.UserAPICheckMinIOVersionHandler = user_api.CheckMinIOVersionHandlerFunc(func(params user_api.CheckMinIOVersionParams) middleware.Responder {
|
||||
versionResponse, err := getVersionResponse()
|
||||
versionResponse, err := getVersionResponse(params)
|
||||
if err != nil {
|
||||
return user_api.NewCheckMinIOVersionDefault(int(err.Code)).WithPayload(err)
|
||||
}
|
||||
@@ -39,13 +43,15 @@ func registerVersionHandlers(api *operations.OperatorAPI) {
|
||||
}
|
||||
|
||||
// getSessionResponse parse the token of the current session and returns a list of allowed actions to render in the UI
|
||||
func getVersionResponse() (*models.CheckOperatorVersionResponse, *models.Error) {
|
||||
ver, err := utils.GetLatestMinIOImage(&utils.HTTPClient{
|
||||
func getVersionResponse(params user_api.CheckMinIOVersionParams) (*models.CheckOperatorVersionResponse, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
ver, err := utils.GetLatestMinIOImage(&xhttp.Client{
|
||||
Client: &http.Client{
|
||||
Timeout: 15 * time.Second,
|
||||
}})
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, errors.ErrorWithContext(ctx, err)
|
||||
}
|
||||
return &models.CheckOperatorVersionResponse{
|
||||
LatestVersion: *ver,
|
||||
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
errors "github.com/minio/console/restapi"
|
||||
|
||||
miniov1 "github.com/minio/operator/pkg/apis/minio.min.io/v1"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
@@ -33,7 +35,7 @@ import (
|
||||
|
||||
func registerVolumesHandlers(api *operations.OperatorAPI) {
|
||||
api.OperatorAPIListPVCsHandler = operator_api.ListPVCsHandlerFunc(func(params operator_api.ListPVCsParams, session *models.Principal) middleware.Responder {
|
||||
payload, err := getPVCsResponse(session)
|
||||
payload, err := getPVCsResponse(session, params)
|
||||
|
||||
if err != nil {
|
||||
return operator_api.NewListPVCsDefault(int(err.Code)).WithPayload(err)
|
||||
@@ -72,13 +74,13 @@ func registerVolumesHandlers(api *operations.OperatorAPI) {
|
||||
|
||||
}
|
||||
|
||||
func getPVCsResponse(session *models.Principal) (*models.ListPVCsResponse, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
func getPVCsResponse(session *models.Principal, params operator_api.ListPVCsParams) (*models.ListPVCsResponse, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
clientset, err := cluster.K8sClient(session.STSSessionToken)
|
||||
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, errors.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
// Filter Tenant PVCs. They keep their v1 tenant annotation
|
||||
@@ -90,7 +92,7 @@ func getPVCsResponse(session *models.Principal) (*models.ListPVCsResponse, *mode
|
||||
listAllPvcs, err2 := clientset.CoreV1().PersistentVolumeClaims("").List(ctx, listOpts)
|
||||
|
||||
if err2 != nil {
|
||||
return nil, prepareError(err2)
|
||||
return nil, errors.ErrorWithContext(ctx, err2)
|
||||
}
|
||||
|
||||
var ListPVCs []*models.PvcsListResponse
|
||||
@@ -121,12 +123,12 @@ func getPVCsResponse(session *models.Principal) (*models.ListPVCsResponse, *mode
|
||||
}
|
||||
|
||||
func getPVCsForTenantResponse(session *models.Principal, params operator_api.ListPVCsForTenantParams) (*models.ListPVCsResponse, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
clientset, err := cluster.K8sClient(session.STSSessionToken)
|
||||
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, errors.ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
// Filter Tenant PVCs. They keep their v1 tenant annotation
|
||||
@@ -138,7 +140,7 @@ func getPVCsForTenantResponse(session *models.Principal, params operator_api.Lis
|
||||
listAllPvcs, err2 := clientset.CoreV1().PersistentVolumeClaims(params.Namespace).List(ctx, listOpts)
|
||||
|
||||
if err2 != nil {
|
||||
return nil, prepareError(err2)
|
||||
return nil, errors.ErrorWithContext(ctx, err2)
|
||||
}
|
||||
|
||||
var ListPVCs []*models.PvcsListResponse
|
||||
@@ -169,37 +171,37 @@ func getPVCsForTenantResponse(session *models.Principal, params operator_api.Lis
|
||||
}
|
||||
|
||||
func getDeletePVCResponse(session *models.Principal, params operator_api.DeletePVCParams) *models.Error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
// get Kubernetes Client
|
||||
clientset, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return prepareError(err)
|
||||
return errors.ErrorWithContext(ctx, err)
|
||||
}
|
||||
listOpts := metav1.ListOptions{
|
||||
LabelSelector: fmt.Sprintf("v1.min.io/tenant=%s", params.Tenant),
|
||||
FieldSelector: fmt.Sprintf("metadata.name=%s", params.PVCName),
|
||||
}
|
||||
if err = clientset.CoreV1().PersistentVolumeClaims(params.Namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, listOpts); err != nil {
|
||||
return prepareError(err)
|
||||
return errors.ErrorWithContext(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPVCEventsResponse(session *models.Principal, params operator_api.GetPVCEventsParams) (models.EventListWrapper, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
clientset, err := cluster.K8sClient(session.STSSessionToken)
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, errors.ErrorWithContext(ctx, err)
|
||||
}
|
||||
PVC, err := clientset.CoreV1().PersistentVolumeClaims(params.Namespace).Get(ctx, params.PVCName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, errors.ErrorWithContext(ctx, err)
|
||||
}
|
||||
events, err := clientset.CoreV1().Events(params.Namespace).List(ctx, metav1.ListOptions{FieldSelector: fmt.Sprintf("involvedObject.uid=%s", PVC.UID)})
|
||||
if err != nil {
|
||||
return nil, prepareError(err)
|
||||
return nil, errors.ErrorWithContext(ctx, err)
|
||||
}
|
||||
retval := models.EventListWrapper{}
|
||||
for i := 0; i < len(events.Items); i++ {
|
||||
|
||||
@@ -89,11 +89,14 @@ func SessionTokenAuthenticate(token string) (*TokenClaims, error) {
|
||||
if token == "" {
|
||||
return nil, ErrNoAuthToken
|
||||
}
|
||||
// decrypt encrypted token
|
||||
claimTokens, err := decryptClaims(token)
|
||||
decryptedToken, err := DecryptToken(token)
|
||||
if err != nil {
|
||||
// we print decryption token error information for debugging purposes
|
||||
// we return a generic error that doesn't give any information to attackers
|
||||
// fail decrypting token
|
||||
return nil, errReadingToken
|
||||
}
|
||||
claimTokens, err := ParseClaimsFromToken(string(decryptedToken))
|
||||
if err != nil {
|
||||
// fail unmarshalling token into data structure
|
||||
return nil, errReadingToken
|
||||
}
|
||||
// claimsTokens contains the decrypted JWT for Console
|
||||
@@ -136,21 +139,26 @@ func encryptClaims(credentials *TokenClaims) (string, error) {
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// decryptClaims() receives base64 encoded ciphertext, decode it, decrypt it (AES-GCM) and produces a *TokenClaims object
|
||||
func decryptClaims(ciphertext string) (*TokenClaims, error) {
|
||||
// ParseClaimsFromToken receive token claims in string format, then unmarshal them to produce a *TokenClaims object
|
||||
func ParseClaimsFromToken(claims string) (*TokenClaims, error) {
|
||||
tokenClaims := &TokenClaims{}
|
||||
if err := json.Unmarshal([]byte(claims), tokenClaims); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tokenClaims, nil
|
||||
}
|
||||
|
||||
// DecryptToken receives base64 encoded ciphertext, decode it, decrypt it (AES-GCM) and produces []byte
|
||||
func DecryptToken(ciphertext string) (plaintext []byte, err error) {
|
||||
decoded, err := base64.StdEncoding.DecodeString(ciphertext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plaintext, err := decrypt(decoded, []byte{})
|
||||
plaintext, err = decrypt(decoded, []byte{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tokenClaims := &TokenClaims{}
|
||||
if err = json.Unmarshal(plaintext, tokenClaims); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tokenClaims, nil
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -332,3 +332,12 @@ func GetAllCertificatesAndCAs() (*x509.CertPool, []*x509.Certificate, *xcerts.Ma
|
||||
}
|
||||
return rootCAs, publicCerts, certsManager, nil
|
||||
}
|
||||
|
||||
// EnsureCertAndKey checks if both client certificate and key paths are provided
|
||||
func EnsureCertAndKey(clientCert, clientKey string) error {
|
||||
if (clientCert != "" && clientKey == "") ||
|
||||
(clientCert == "" && clientKey != "") {
|
||||
return errors.New("cert and key must be specified as a pair")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
23
pkg/http/headers.go
Normal file
23
pkg/http/headers.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package http
|
||||
|
||||
// Standard S3 HTTP response constants
|
||||
const (
|
||||
ETag = "ETag"
|
||||
ContentType = "Content-Type"
|
||||
)
|
||||
@@ -14,40 +14,61 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package utils
|
||||
package http
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// HTTPClientI interface with all functions to be implemented
|
||||
// ClientI interface with all functions to be implemented
|
||||
// by mock when testing, it should include all HttpClient respective api calls
|
||||
// that are used within this project.
|
||||
type HTTPClientI interface {
|
||||
type ClientI interface {
|
||||
Get(url string) (resp *http.Response, err error)
|
||||
Post(url, contentType string, body io.Reader) (resp *http.Response, err error)
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// HTTPClient Interface implementation
|
||||
// Client is an HTTP Interface implementation
|
||||
//
|
||||
// Define the structure of a http client and define the functions that are actually used
|
||||
type HTTPClient struct {
|
||||
type Client struct {
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
// Get implements http.Client.Get()
|
||||
func (c *HTTPClient) Get(url string) (resp *http.Response, err error) {
|
||||
func (c *Client) Get(url string) (resp *http.Response, err error) {
|
||||
return c.Client.Get(url)
|
||||
}
|
||||
|
||||
// Post implements http.Client.Post()
|
||||
func (c *HTTPClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) {
|
||||
func (c *Client) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) {
|
||||
return c.Client.Post(url, contentType, body)
|
||||
}
|
||||
|
||||
// Do implement http.Client.Do()
|
||||
func (c *HTTPClient) Do(req *http.Request) (*http.Response, error) {
|
||||
func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
||||
return c.Client.Do(req)
|
||||
}
|
||||
|
||||
// DrainBody close non nil response with any response Body.
|
||||
// convenient wrapper to drain any remaining data on response body.
|
||||
//
|
||||
// Subsequently this allows golang http RoundTripper
|
||||
// to re-use the same connection for future requests.
|
||||
func DrainBody(respBody io.ReadCloser) {
|
||||
// Callers should close resp.Body when done reading from it.
|
||||
// If resp.Body is not closed, the Client's underlying RoundTripper
|
||||
// (typically Transport) may not be able to re-use a persistent TCP
|
||||
// connection to the server for a subsequent "keep-alive" request.
|
||||
if respBody != nil {
|
||||
// Drain any remaining Body and then close the connection.
|
||||
// Without this closing connection would disallow re-using
|
||||
// the same connection for future uses.
|
||||
// - http://stackoverflow.com/a/17961593/4465767
|
||||
defer respBody.Close()
|
||||
io.Copy(ioutil.Discard, respBody)
|
||||
}
|
||||
}
|
||||
228
pkg/logger/audit.go
Normal file
228
pkg/logger/audit.go
Normal file
@@ -0,0 +1,228 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package logger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/minio/console/pkg/utils"
|
||||
|
||||
"github.com/minio/console/pkg/logger/message/audit"
|
||||
)
|
||||
|
||||
// ResponseWriter - is a wrapper to trap the http response status code.
|
||||
type ResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
StatusCode int
|
||||
// Log body of 4xx or 5xx responses
|
||||
LogErrBody bool
|
||||
// Log body of all responses
|
||||
LogAllBody bool
|
||||
|
||||
TimeToFirstByte time.Duration
|
||||
StartTime time.Time
|
||||
// number of bytes written
|
||||
bytesWritten int
|
||||
// Internal recording buffer
|
||||
headers bytes.Buffer
|
||||
body bytes.Buffer
|
||||
// Indicate if headers are written in the log
|
||||
headersLogged bool
|
||||
}
|
||||
|
||||
// NewResponseWriter - returns a wrapped response writer to trap
|
||||
// http status codes for auditing purposes.
|
||||
func NewResponseWriter(w http.ResponseWriter) *ResponseWriter {
|
||||
return &ResponseWriter{
|
||||
ResponseWriter: w,
|
||||
StatusCode: http.StatusOK,
|
||||
StartTime: time.Now().UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
func (lrw *ResponseWriter) Write(p []byte) (int, error) {
|
||||
if !lrw.headersLogged {
|
||||
// We assume the response code to be '200 OK' when WriteHeader() is not called,
|
||||
// that way following Golang HTTP response behavior.
|
||||
lrw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
n, err := lrw.ResponseWriter.Write(p)
|
||||
lrw.bytesWritten += n
|
||||
if lrw.TimeToFirstByte == 0 {
|
||||
lrw.TimeToFirstByte = time.Now().UTC().Sub(lrw.StartTime)
|
||||
}
|
||||
if (lrw.LogErrBody && lrw.StatusCode >= http.StatusBadRequest) || lrw.LogAllBody {
|
||||
// Always logging error responses.
|
||||
lrw.body.Write(p)
|
||||
}
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Write the headers into the given buffer
|
||||
func (lrw *ResponseWriter) writeHeaders(w io.Writer, statusCode int, headers http.Header) {
|
||||
n, _ := fmt.Fprintf(w, "%d %s\n", statusCode, http.StatusText(statusCode))
|
||||
lrw.bytesWritten += n
|
||||
for k, v := range headers {
|
||||
n, _ := fmt.Fprintf(w, "%s: %s\n", k, v[0])
|
||||
lrw.bytesWritten += n
|
||||
}
|
||||
}
|
||||
|
||||
// BodyPlaceHolder returns a dummy body placeholder
|
||||
var BodyPlaceHolder = []byte("<BODY>")
|
||||
|
||||
// Body - Return response body.
|
||||
func (lrw *ResponseWriter) Body() []byte {
|
||||
// If there was an error response or body logging is enabled
|
||||
// then we return the body contents
|
||||
if (lrw.LogErrBody && lrw.StatusCode >= http.StatusBadRequest) || lrw.LogAllBody {
|
||||
return lrw.body.Bytes()
|
||||
}
|
||||
// ... otherwise we return the <BODY> place holder
|
||||
return BodyPlaceHolder
|
||||
}
|
||||
|
||||
// WriteHeader - writes http status code
|
||||
func (lrw *ResponseWriter) WriteHeader(code int) {
|
||||
if !lrw.headersLogged {
|
||||
lrw.StatusCode = code
|
||||
lrw.writeHeaders(&lrw.headers, code, lrw.ResponseWriter.Header())
|
||||
lrw.headersLogged = true
|
||||
lrw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
}
|
||||
|
||||
// Flush - Calls the underlying Flush.
|
||||
func (lrw *ResponseWriter) Flush() {
|
||||
lrw.ResponseWriter.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// Size - reutrns the number of bytes written
|
||||
func (lrw *ResponseWriter) Size() int {
|
||||
return lrw.bytesWritten
|
||||
}
|
||||
|
||||
// SetAuditEntry sets Audit info in the context.
|
||||
func SetAuditEntry(ctx context.Context, audit *audit.Entry) context.Context {
|
||||
if ctx == nil {
|
||||
LogIf(context.Background(), fmt.Errorf("context is nil"))
|
||||
return nil
|
||||
}
|
||||
return context.WithValue(ctx, utils.ContextAuditKey, audit)
|
||||
}
|
||||
|
||||
// GetAuditEntry returns Audit entry if set.
|
||||
func GetAuditEntry(ctx context.Context) *audit.Entry {
|
||||
if ctx != nil {
|
||||
r, ok := ctx.Value(utils.ContextAuditKey).(*audit.Entry)
|
||||
if ok {
|
||||
return r
|
||||
}
|
||||
r = &audit.Entry{
|
||||
Version: audit.Version,
|
||||
//DeploymentID: globalDeploymentID,
|
||||
Time: time.Now().UTC(),
|
||||
}
|
||||
SetAuditEntry(ctx, r)
|
||||
return r
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuditLog - logs audit logs to all audit targets.
|
||||
func AuditLog(ctx context.Context, w *ResponseWriter, r *http.Request, reqClaims map[string]interface{}, filterKeys ...string) {
|
||||
// Fast exit if there is not audit target configured
|
||||
if atomic.LoadInt32(&nAuditTargets) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var entry audit.Entry
|
||||
|
||||
if w != nil && r != nil {
|
||||
reqInfo := GetReqInfo(ctx)
|
||||
if reqInfo == nil {
|
||||
return
|
||||
}
|
||||
entry = audit.ToEntry(w, r, reqClaims, GetGlobalDeploymentID())
|
||||
// indicates all requests for this API call are inbound
|
||||
entry.Trigger = "incoming"
|
||||
|
||||
for _, filterKey := range filterKeys {
|
||||
delete(entry.ReqClaims, filterKey)
|
||||
delete(entry.ReqQuery, filterKey)
|
||||
delete(entry.ReqHeader, filterKey)
|
||||
delete(entry.RespHeader, filterKey)
|
||||
}
|
||||
|
||||
var (
|
||||
statusCode int
|
||||
timeToResponse time.Duration
|
||||
timeToFirstByte time.Duration
|
||||
outputBytes int64 = -1 // -1: unknown output bytes
|
||||
)
|
||||
|
||||
if w != nil {
|
||||
statusCode = w.StatusCode
|
||||
timeToResponse = time.Now().UTC().Sub(w.StartTime)
|
||||
timeToFirstByte = w.TimeToFirstByte
|
||||
outputBytes = int64(w.Size())
|
||||
}
|
||||
|
||||
entry.API.Path = r.URL.Path
|
||||
|
||||
entry.API.Status = http.StatusText(statusCode)
|
||||
entry.API.StatusCode = statusCode
|
||||
entry.API.Method = r.Method
|
||||
entry.API.InputBytes = r.ContentLength
|
||||
entry.API.OutputBytes = outputBytes
|
||||
entry.RequestID = reqInfo.RequestID
|
||||
|
||||
entry.API.TimeToResponse = strconv.FormatInt(timeToResponse.Nanoseconds(), 10) + "ns"
|
||||
entry.Tags = reqInfo.GetTagsMap()
|
||||
// ttfb will be recorded only for GET requests, Ignore such cases where ttfb will be empty.
|
||||
if timeToFirstByte != 0 {
|
||||
entry.API.TimeToFirstByte = strconv.FormatInt(timeToFirstByte.Nanoseconds(), 10) + "ns"
|
||||
}
|
||||
} else {
|
||||
auditEntry := GetAuditEntry(ctx)
|
||||
if auditEntry != nil {
|
||||
entry = *auditEntry
|
||||
}
|
||||
}
|
||||
|
||||
if anonFlag {
|
||||
entry.SessionID = hashString(entry.SessionID)
|
||||
entry.RemoteHost = hashString(entry.RemoteHost)
|
||||
}
|
||||
|
||||
// Send audit logs only to http targets.
|
||||
for _, t := range AuditTargets() {
|
||||
if err := t.Send(entry, string(All)); err != nil {
|
||||
LogAlwaysIf(context.Background(), fmt.Errorf("event(%v) was not sent to Audit target (%v): %v", entry, t, err), All)
|
||||
}
|
||||
}
|
||||
}
|
||||
60
pkg/logger/color/color.go
Normal file
60
pkg/logger/color/color.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package color
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// global colors.
|
||||
var (
|
||||
// Check if we stderr, stdout are dumb terminals, we do not apply
|
||||
// ansi coloring on dumb terminals.
|
||||
IsTerminal = func() bool {
|
||||
return !color.NoColor
|
||||
}
|
||||
|
||||
Bold = func() func(a ...interface{}) string {
|
||||
if IsTerminal() {
|
||||
return color.New(color.Bold).SprintFunc()
|
||||
}
|
||||
return fmt.Sprint
|
||||
}()
|
||||
|
||||
FgRed = func() func(a ...interface{}) string {
|
||||
if IsTerminal() {
|
||||
return color.New(color.FgRed).SprintFunc()
|
||||
}
|
||||
return fmt.Sprint
|
||||
}()
|
||||
|
||||
BgRed = func() func(format string, a ...interface{}) string {
|
||||
if IsTerminal() {
|
||||
return color.New(color.BgRed).SprintfFunc()
|
||||
}
|
||||
return fmt.Sprintf
|
||||
}()
|
||||
|
||||
FgWhite = func() func(format string, a ...interface{}) string {
|
||||
if IsTerminal() {
|
||||
return color.New(color.FgWhite).SprintfFunc()
|
||||
}
|
||||
return fmt.Sprintf
|
||||
}()
|
||||
)
|
||||
213
pkg/logger/config.go
Normal file
213
pkg/logger/config.go
Normal file
@@ -0,0 +1,213 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package logger
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/minio/console/pkg/logger/config"
|
||||
"github.com/minio/console/pkg/logger/target/http"
|
||||
"github.com/minio/pkg/env"
|
||||
)
|
||||
|
||||
// NewConfig - initialize new logger config.
|
||||
func NewConfig() Config {
|
||||
cfg := Config{
|
||||
HTTP: make(map[string]http.Config),
|
||||
AuditWebhook: make(map[string]http.Config),
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func lookupLoggerWebhookConfig() (Config, error) {
|
||||
cfg := NewConfig()
|
||||
envs := env.List(EnvLoggerWebhookEndpoint)
|
||||
var loggerTargets []string
|
||||
for _, k := range envs {
|
||||
target := strings.TrimPrefix(k, EnvLoggerWebhookEndpoint+config.Default)
|
||||
if target == EnvLoggerWebhookEndpoint {
|
||||
target = config.Default
|
||||
}
|
||||
loggerTargets = append(loggerTargets, target)
|
||||
}
|
||||
|
||||
// Load HTTP logger from the environment if found
|
||||
for _, target := range loggerTargets {
|
||||
if v, ok := cfg.HTTP[target]; ok && v.Enabled {
|
||||
// This target is already enabled using the
|
||||
// legacy environment variables, ignore.
|
||||
continue
|
||||
}
|
||||
enableEnv := EnvLoggerWebhookEnable
|
||||
if target != config.Default {
|
||||
enableEnv = EnvLoggerWebhookEnable + config.Default + target
|
||||
}
|
||||
enable, err := config.ParseBool(env.Get(enableEnv, ""))
|
||||
if err != nil || !enable {
|
||||
continue
|
||||
}
|
||||
endpointEnv := EnvLoggerWebhookEndpoint
|
||||
if target != config.Default {
|
||||
endpointEnv = EnvLoggerWebhookEndpoint + config.Default + target
|
||||
}
|
||||
authTokenEnv := EnvLoggerWebhookAuthToken
|
||||
if target != config.Default {
|
||||
authTokenEnv = EnvLoggerWebhookAuthToken + config.Default + target
|
||||
}
|
||||
clientCertEnv := EnvLoggerWebhookClientCert
|
||||
if target != config.Default {
|
||||
clientCertEnv = EnvLoggerWebhookClientCert + config.Default + target
|
||||
}
|
||||
clientKeyEnv := EnvLoggerWebhookClientKey
|
||||
if target != config.Default {
|
||||
clientKeyEnv = EnvLoggerWebhookClientKey + config.Default + target
|
||||
}
|
||||
err = config.EnsureCertAndKey(env.Get(clientCertEnv, ""), env.Get(clientKeyEnv, ""))
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
queueSizeEnv := EnvLoggerWebhookQueueSize
|
||||
if target != config.Default {
|
||||
queueSizeEnv = EnvLoggerWebhookQueueSize + config.Default + target
|
||||
}
|
||||
queueSize, err := strconv.Atoi(env.Get(queueSizeEnv, "100000"))
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
if queueSize <= 0 {
|
||||
return cfg, errors.New("invalid queue_size value")
|
||||
}
|
||||
cfg.HTTP[target] = http.Config{
|
||||
Enabled: true,
|
||||
Endpoint: env.Get(endpointEnv, ""),
|
||||
AuthToken: env.Get(authTokenEnv, ""),
|
||||
ClientCert: env.Get(clientCertEnv, ""),
|
||||
ClientKey: env.Get(clientKeyEnv, ""),
|
||||
QueueSize: queueSize,
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func lookupAuditWebhookConfig() (Config, error) {
|
||||
cfg := NewConfig()
|
||||
var loggerAuditTargets []string
|
||||
envs := env.List(EnvAuditWebhookEndpoint)
|
||||
for _, k := range envs {
|
||||
target := strings.TrimPrefix(k, EnvAuditWebhookEndpoint+config.Default)
|
||||
if target == EnvAuditWebhookEndpoint {
|
||||
target = config.Default
|
||||
}
|
||||
loggerAuditTargets = append(loggerAuditTargets, target)
|
||||
}
|
||||
|
||||
for _, target := range loggerAuditTargets {
|
||||
if v, ok := cfg.AuditWebhook[target]; ok && v.Enabled {
|
||||
// This target is already enabled using the
|
||||
// legacy environment variables, ignore.
|
||||
continue
|
||||
}
|
||||
enableEnv := EnvAuditWebhookEnable
|
||||
if target != config.Default {
|
||||
enableEnv = EnvAuditWebhookEnable + config.Default + target
|
||||
}
|
||||
enable, err := config.ParseBool(env.Get(enableEnv, ""))
|
||||
if err != nil || !enable {
|
||||
continue
|
||||
}
|
||||
endpointEnv := EnvAuditWebhookEndpoint
|
||||
if target != config.Default {
|
||||
endpointEnv = EnvAuditWebhookEndpoint + config.Default + target
|
||||
}
|
||||
authTokenEnv := EnvAuditWebhookAuthToken
|
||||
if target != config.Default {
|
||||
authTokenEnv = EnvAuditWebhookAuthToken + config.Default + target
|
||||
}
|
||||
clientCertEnv := EnvAuditWebhookClientCert
|
||||
if target != config.Default {
|
||||
clientCertEnv = EnvAuditWebhookClientCert + config.Default + target
|
||||
}
|
||||
clientKeyEnv := EnvAuditWebhookClientKey
|
||||
if target != config.Default {
|
||||
clientKeyEnv = EnvAuditWebhookClientKey + config.Default + target
|
||||
}
|
||||
err = config.EnsureCertAndKey(env.Get(clientCertEnv, ""), env.Get(clientKeyEnv, ""))
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
queueSizeEnv := EnvAuditWebhookQueueSize
|
||||
if target != config.Default {
|
||||
queueSizeEnv = EnvAuditWebhookQueueSize + config.Default + target
|
||||
}
|
||||
queueSize, err := strconv.Atoi(env.Get(queueSizeEnv, "100000"))
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
if queueSize <= 0 {
|
||||
return cfg, errors.New("invalid queue_size value")
|
||||
}
|
||||
cfg.AuditWebhook[target] = http.Config{
|
||||
Enabled: true,
|
||||
Endpoint: env.Get(endpointEnv, ""),
|
||||
AuthToken: env.Get(authTokenEnv, ""),
|
||||
ClientCert: env.Get(clientCertEnv, ""),
|
||||
ClientKey: env.Get(clientKeyEnv, ""),
|
||||
QueueSize: queueSize,
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// LookupConfigForSubSys - lookup logger config, override with ENVs if set, for the given sub-system
|
||||
func LookupConfigForSubSys(subSys string) (cfg Config, err error) {
|
||||
switch subSys {
|
||||
case config.LoggerWebhookSubSys:
|
||||
if cfg, err = lookupLoggerWebhookConfig(); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
case config.AuditWebhookSubSys:
|
||||
if cfg, err = lookupAuditWebhookConfig(); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// GetGlobalDeploymentID :
|
||||
func GetGlobalDeploymentID() string {
|
||||
if globalDeploymentID != "" {
|
||||
return globalDeploymentID
|
||||
}
|
||||
globalDeploymentID = env.Get(EnvGlobalDeploymentID, mustGetUUID())
|
||||
return globalDeploymentID
|
||||
}
|
||||
|
||||
// mustGetUUID - get a random UUID.
|
||||
func mustGetUUID() string {
|
||||
u, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
CriticalIf(GlobalContext, err)
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
80
pkg/logger/config/bool-flag.go
Normal file
80
pkg/logger/config/bool-flag.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BoolFlag - wrapper bool type.
|
||||
type BoolFlag bool
|
||||
|
||||
// String - returns string of BoolFlag.
|
||||
func (bf BoolFlag) String() string {
|
||||
if bf {
|
||||
return "on"
|
||||
}
|
||||
|
||||
return "off"
|
||||
}
|
||||
|
||||
// MarshalJSON - converts BoolFlag into JSON data.
|
||||
func (bf BoolFlag) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(bf.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON - parses given data into BoolFlag.
|
||||
func (bf *BoolFlag) UnmarshalJSON(data []byte) (err error) {
|
||||
var s string
|
||||
if err = json.Unmarshal(data, &s); err == nil {
|
||||
b := BoolFlag(true)
|
||||
if s == "" {
|
||||
// Empty string is treated as valid.
|
||||
*bf = b
|
||||
} else if b, err = ParseBoolFlag(s); err == nil {
|
||||
*bf = b
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ParseBool returns the boolean value represented by the string.
|
||||
func ParseBool(str string) (bool, error) {
|
||||
switch str {
|
||||
case "1", "t", "T", "true", "TRUE", "True", "on", "ON", "On":
|
||||
return true, nil
|
||||
case "0", "f", "F", "false", "FALSE", "False", "off", "OFF", "Off":
|
||||
return false, nil
|
||||
}
|
||||
if strings.EqualFold(str, "enabled") {
|
||||
return true, nil
|
||||
}
|
||||
if strings.EqualFold(str, "disabled") {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("ParseBool: parsing '%s': %w", str, strconv.ErrSyntax)
|
||||
}
|
||||
|
||||
// ParseBoolFlag - parses string into BoolFlag.
|
||||
func ParseBoolFlag(s string) (bf BoolFlag, err error) {
|
||||
b, err := ParseBool(s)
|
||||
return BoolFlag(b), err
|
||||
}
|
||||
126
pkg/logger/config/bool-flag_test.go
Normal file
126
pkg/logger/config/bool-flag_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
// Test BoolFlag.String()
|
||||
func TestBoolFlagString(t *testing.T) {
|
||||
var bf BoolFlag
|
||||
|
||||
testCases := []struct {
|
||||
flag BoolFlag
|
||||
expectedResult string
|
||||
}{
|
||||
{bf, "off"},
|
||||
{BoolFlag(true), "on"},
|
||||
{BoolFlag(false), "off"},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
str := testCase.flag.String()
|
||||
if testCase.expectedResult != str {
|
||||
t.Fatalf("expected: %v, got: %v", testCase.expectedResult, str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test BoolFlag.MarshalJSON()
|
||||
func TestBoolFlagMarshalJSON(t *testing.T) {
|
||||
var bf BoolFlag
|
||||
|
||||
testCases := []struct {
|
||||
flag BoolFlag
|
||||
expectedResult string
|
||||
}{
|
||||
{bf, `"off"`},
|
||||
{BoolFlag(true), `"on"`},
|
||||
{BoolFlag(false), `"off"`},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
data, _ := testCase.flag.MarshalJSON()
|
||||
if testCase.expectedResult != string(data) {
|
||||
t.Fatalf("expected: %v, got: %v", testCase.expectedResult, string(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test BoolFlag.UnmarshalJSON()
|
||||
func TestBoolFlagUnmarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
data []byte
|
||||
expectedResult BoolFlag
|
||||
expectedErr bool
|
||||
}{
|
||||
{[]byte(`{}`), BoolFlag(false), true},
|
||||
{[]byte(`["on"]`), BoolFlag(false), true},
|
||||
{[]byte(`"junk"`), BoolFlag(false), true},
|
||||
{[]byte(`""`), BoolFlag(true), false},
|
||||
{[]byte(`"on"`), BoolFlag(true), false},
|
||||
{[]byte(`"off"`), BoolFlag(false), false},
|
||||
{[]byte(`"true"`), BoolFlag(true), false},
|
||||
{[]byte(`"false"`), BoolFlag(false), false},
|
||||
{[]byte(`"ON"`), BoolFlag(true), false},
|
||||
{[]byte(`"OFF"`), BoolFlag(false), false},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
var flag BoolFlag
|
||||
err := (&flag).UnmarshalJSON(testCase.data)
|
||||
if !testCase.expectedErr && err != nil {
|
||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||
}
|
||||
if testCase.expectedErr && err == nil {
|
||||
t.Fatalf("error: expected error, got = <nil>")
|
||||
}
|
||||
if err == nil && testCase.expectedResult != flag {
|
||||
t.Fatalf("result: expected: %v, got: %v", testCase.expectedResult, flag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test ParseBoolFlag()
|
||||
func TestParseBoolFlag(t *testing.T) {
|
||||
testCases := []struct {
|
||||
flagStr string
|
||||
expectedResult BoolFlag
|
||||
expectedErr bool
|
||||
}{
|
||||
{"", BoolFlag(false), true},
|
||||
{"junk", BoolFlag(false), true},
|
||||
{"true", BoolFlag(true), false},
|
||||
{"false", BoolFlag(false), false},
|
||||
{"ON", BoolFlag(true), false},
|
||||
{"OFF", BoolFlag(false), false},
|
||||
{"on", BoolFlag(true), false},
|
||||
{"off", BoolFlag(false), false},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
bf, err := ParseBoolFlag(testCase.flagStr)
|
||||
if !testCase.expectedErr && err != nil {
|
||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||
}
|
||||
if testCase.expectedErr && err == nil {
|
||||
t.Fatalf("error: expected error, got = <nil>")
|
||||
}
|
||||
if err == nil && testCase.expectedResult != bf {
|
||||
t.Fatalf("result: expected: %v, got: %v", testCase.expectedResult, bf)
|
||||
}
|
||||
}
|
||||
}
|
||||
30
pkg/logger/config/certs.go
Normal file
30
pkg/logger/config/certs.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// EnsureCertAndKey checks if both client certificate and key paths are provided
|
||||
func EnsureCertAndKey(clientCert, clientKey string) error {
|
||||
if (clientCert != "" && clientKey == "") ||
|
||||
(clientCert == "" && clientKey != "") {
|
||||
return errors.New("cert and key must be specified as a pair")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
34
pkg/logger/config/config.go
Normal file
34
pkg/logger/config/config.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/minio/madmin-go"
|
||||
)
|
||||
|
||||
// Default keys
|
||||
const (
|
||||
Default = madmin.Default
|
||||
Enable = madmin.EnableKey
|
||||
License = "license" // Deprecated Dec 2021
|
||||
)
|
||||
|
||||
// Top level config constants.
|
||||
const (
|
||||
LoggerWebhookSubSys = "logger_webhook"
|
||||
AuditWebhookSubSys = "audit_webhook"
|
||||
)
|
||||
223
pkg/logger/console.go
Normal file
223
pkg/logger/console.go
Normal file
@@ -0,0 +1,223 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package logger
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/console/pkg/logger/color"
|
||||
"github.com/minio/console/pkg/logger/message/log"
|
||||
c "github.com/minio/pkg/console"
|
||||
)
|
||||
|
||||
// ConsoleLoggerTgt is a stringified value to represent console logging
|
||||
const ConsoleLoggerTgt = "console+http"
|
||||
|
||||
// Logger interface describes the methods that need to be implemented to satisfy the interface requirements.
|
||||
type Logger interface {
|
||||
json(msg string, args ...interface{})
|
||||
quiet(msg string, args ...interface{})
|
||||
pretty(msg string, args ...interface{})
|
||||
}
|
||||
|
||||
func consoleLog(console Logger, msg string, args ...interface{}) {
|
||||
switch {
|
||||
case jsonFlag:
|
||||
// Strip escape control characters from json message
|
||||
msg = ansiRE.ReplaceAllLiteralString(msg, "")
|
||||
console.json(msg, args...)
|
||||
case quietFlag:
|
||||
console.quiet(msg+"\n", args...)
|
||||
default:
|
||||
console.pretty(msg+"\n", args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Fatal prints only fatal errors message with no stack trace
|
||||
// it will be called for input validation failures
|
||||
func Fatal(err error, msg string, data ...interface{}) {
|
||||
fatal(err, msg, data...)
|
||||
}
|
||||
|
||||
func fatal(err error, msg string, data ...interface{}) {
|
||||
var errMsg string
|
||||
if msg != "" {
|
||||
errMsg = errorFmtFunc(fmt.Sprintf(msg, data...), err, jsonFlag)
|
||||
} else {
|
||||
errMsg = err.Error()
|
||||
}
|
||||
consoleLog(fatalMessage, errMsg)
|
||||
}
|
||||
|
||||
var fatalMessage fatalMsg
|
||||
|
||||
type fatalMsg struct{}
|
||||
|
||||
func (f fatalMsg) json(msg string, args ...interface{}) {
|
||||
var message string
|
||||
if msg != "" {
|
||||
message = fmt.Sprintf(msg, args...)
|
||||
} else {
|
||||
message = fmt.Sprint(args...)
|
||||
}
|
||||
logJSON, err := json.Marshal(&log.Entry{
|
||||
Level: FatalLvl.String(),
|
||||
Message: message,
|
||||
Time: time.Now().UTC(),
|
||||
Trace: &log.Trace{Message: message, Source: []string{getSource(6)}},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(string(logJSON))
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func (f fatalMsg) quiet(msg string, args ...interface{}) {
|
||||
f.pretty(msg, args...)
|
||||
}
|
||||
|
||||
var (
|
||||
logTag = "ERROR"
|
||||
logBanner = color.BgRed(color.FgWhite(color.Bold(logTag))) + " "
|
||||
emptyBanner = color.BgRed(strings.Repeat(" ", len(logTag))) + " "
|
||||
bannerWidth = len(logTag) + 1
|
||||
)
|
||||
|
||||
func (f fatalMsg) pretty(msg string, args ...interface{}) {
|
||||
// Build the passed errors message
|
||||
errMsg := fmt.Sprintf(msg, args...)
|
||||
|
||||
tagPrinted := false
|
||||
|
||||
// Print the errors message: the following code takes care
|
||||
// of splitting errors text and always pretty printing the
|
||||
// red banner along with the errors message. Since the errors
|
||||
// message itself contains some colored text, we needed
|
||||
// to use some ANSI control escapes to cursor color state
|
||||
// and freely move in the screen.
|
||||
for _, line := range strings.Split(errMsg, "\n") {
|
||||
if len(line) == 0 {
|
||||
// No more text to print, just quit.
|
||||
break
|
||||
}
|
||||
|
||||
for {
|
||||
// Save the attributes of the current cursor helps
|
||||
// us save the text color of the passed errors message
|
||||
ansiSaveAttributes()
|
||||
// Print banner with or without the log tag
|
||||
if !tagPrinted {
|
||||
c.Print(logBanner)
|
||||
tagPrinted = true
|
||||
} else {
|
||||
c.Print(emptyBanner)
|
||||
}
|
||||
// Restore the text color of the errors message
|
||||
ansiRestoreAttributes()
|
||||
ansiMoveRight(bannerWidth)
|
||||
// Continue errors message printing
|
||||
c.Println(line)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Exit because this is a fatal errors message
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
type infoMsg struct{}
|
||||
|
||||
var info infoMsg
|
||||
|
||||
func (i infoMsg) json(msg string, args ...interface{}) {
|
||||
var message string
|
||||
if msg != "" {
|
||||
message = fmt.Sprintf(msg, args...)
|
||||
} else {
|
||||
message = fmt.Sprint(args...)
|
||||
}
|
||||
logJSON, err := json.Marshal(&log.Entry{
|
||||
Level: InformationLvl.String(),
|
||||
Message: message,
|
||||
Time: time.Now().UTC(),
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(string(logJSON))
|
||||
}
|
||||
|
||||
func (i infoMsg) quiet(msg string, args ...interface{}) {
|
||||
}
|
||||
|
||||
func (i infoMsg) pretty(msg string, args ...interface{}) {
|
||||
if msg == "" {
|
||||
c.Println(args...)
|
||||
}
|
||||
c.Printf(msg, args...)
|
||||
}
|
||||
|
||||
type errorMsg struct{}
|
||||
|
||||
var errorm errorMsg
|
||||
|
||||
func (i errorMsg) json(msg string, args ...interface{}) {
|
||||
var message string
|
||||
if msg != "" {
|
||||
message = fmt.Sprintf(msg, args...)
|
||||
} else {
|
||||
message = fmt.Sprint(args...)
|
||||
}
|
||||
logJSON, err := json.Marshal(&log.Entry{
|
||||
Level: ErrorLvl.String(),
|
||||
Message: message,
|
||||
Time: time.Now().UTC(),
|
||||
Trace: &log.Trace{Message: message, Source: []string{getSource(6)}},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(string(logJSON))
|
||||
}
|
||||
|
||||
func (i errorMsg) quiet(msg string, args ...interface{}) {
|
||||
i.pretty(msg, args...)
|
||||
}
|
||||
|
||||
func (i errorMsg) pretty(msg string, args ...interface{}) {
|
||||
if msg == "" {
|
||||
c.Println(args...)
|
||||
}
|
||||
c.Printf(msg, args...)
|
||||
c.Printf("\n")
|
||||
}
|
||||
|
||||
// Error :
|
||||
func Error(msg string, data ...interface{}) {
|
||||
consoleLog(errorm, msg, data...)
|
||||
}
|
||||
|
||||
// Info :
|
||||
func Info(msg string, data ...interface{}) {
|
||||
consoleLog(info, msg, data...)
|
||||
}
|
||||
56
pkg/logger/const.go
Normal file
56
pkg/logger/const.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/minio/console/pkg/logger/target/http"
|
||||
)
|
||||
|
||||
// Audit/Logger constants
|
||||
const (
|
||||
EnvLoggerJSONEnable = "CONSOLE_LOGGER_JSON_ENABLE"
|
||||
EnvLoggerAnonymousEnable = "CONSOLE_LOGGER_ANONYMOUS_ENABLE"
|
||||
EnvLoggerQuietEnable = "CONSOLE_LOGGER_QUIET_ENABLE"
|
||||
|
||||
EnvGlobalDeploymentID = "CONSOLE_GLOBAL_DEPLOYMENT_ID"
|
||||
EnvLoggerWebhookEnable = "CONSOLE_LOGGER_WEBHOOK_ENABLE"
|
||||
EnvLoggerWebhookEndpoint = "CONSOLE_LOGGER_WEBHOOK_ENDPOINT"
|
||||
EnvLoggerWebhookAuthToken = "CONSOLE_LOGGER_WEBHOOK_AUTH_TOKEN"
|
||||
EnvLoggerWebhookClientCert = "CONSOLE_LOGGER_WEBHOOK_CLIENT_CERT"
|
||||
EnvLoggerWebhookClientKey = "CONSOLE_LOGGER_WEBHOOK_CLIENT_KEY"
|
||||
EnvLoggerWebhookQueueSize = "CONSOLE_LOGGER_WEBHOOK_QUEUE_SIZE"
|
||||
|
||||
EnvAuditWebhookEnable = "CONSOLE_AUDIT_WEBHOOK_ENABLE"
|
||||
EnvAuditWebhookEndpoint = "CONSOLE_AUDIT_WEBHOOK_ENDPOINT"
|
||||
EnvAuditWebhookAuthToken = "CONSOLE_AUDIT_WEBHOOK_AUTH_TOKEN"
|
||||
EnvAuditWebhookClientCert = "CONSOLE_AUDIT_WEBHOOK_CLIENT_CERT"
|
||||
EnvAuditWebhookClientKey = "CONSOLE_AUDIT_WEBHOOK_CLIENT_KEY"
|
||||
EnvAuditWebhookQueueSize = "CONSOLE_AUDIT_WEBHOOK_QUEUE_SIZE"
|
||||
)
|
||||
|
||||
// Config console and http logger targets
|
||||
type Config struct {
|
||||
HTTP map[string]http.Config `json:"http"`
|
||||
AuditWebhook map[string]http.Config `json:"audit"`
|
||||
}
|
||||
|
||||
var (
|
||||
globalDeploymentID string
|
||||
GlobalContext context.Context
|
||||
)
|
||||
480
pkg/logger/logger.go
Normal file
480
pkg/logger/logger.go
Normal file
@@ -0,0 +1,480 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/minio/pkg/env"
|
||||
|
||||
"github.com/minio/console/pkg"
|
||||
"github.com/minio/pkg/certs"
|
||||
|
||||
"github.com/minio/console/pkg/logger/config"
|
||||
"github.com/minio/console/pkg/logger/message/log"
|
||||
"github.com/minio/highwayhash"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
)
|
||||
|
||||
// HighwayHash key for logging in anonymous mode
|
||||
var magicHighwayHash256Key = []byte("\x4b\xe7\x34\xfa\x8e\x23\x8a\xcd\x26\x3e\x83\xe6\xbb\x96\x85\x52\x04\x0f\x93\x5d\xa3\x9f\x44\x14\x97\xe0\x9d\x13\x22\xde\x36\xa0")
|
||||
|
||||
// Disable disables all logging, false by default. (used for "go test")
|
||||
var Disable = false
|
||||
|
||||
// Level type
|
||||
type Level int8
|
||||
|
||||
// Enumerated level types
|
||||
const (
|
||||
InformationLvl Level = iota + 1
|
||||
ErrorLvl
|
||||
FatalLvl
|
||||
)
|
||||
|
||||
var trimStrings []string
|
||||
|
||||
// TimeFormat - logging time format.
|
||||
const TimeFormat string = "15:04:05 MST 01/02/2006"
|
||||
|
||||
var matchingFuncNames = [...]string{
|
||||
"http.HandlerFunc.ServeHTTP",
|
||||
"cmd.serverMain",
|
||||
"cmd.StartGateway",
|
||||
// add more here ..
|
||||
}
|
||||
|
||||
func (level Level) String() string {
|
||||
var lvlStr string
|
||||
switch level {
|
||||
case InformationLvl:
|
||||
lvlStr = "INFO"
|
||||
case ErrorLvl:
|
||||
lvlStr = "ERROR"
|
||||
case FatalLvl:
|
||||
lvlStr = "FATAL"
|
||||
}
|
||||
return lvlStr
|
||||
}
|
||||
|
||||
// quietFlag: Hide startup messages if enabled
|
||||
// jsonFlag: Display in JSON format, if enabled
|
||||
var (
|
||||
quietFlag, jsonFlag, anonFlag bool
|
||||
// Custom function to format errors
|
||||
errorFmtFunc func(string, error, bool) string
|
||||
)
|
||||
|
||||
// EnableQuiet - turns quiet option on.
|
||||
func EnableQuiet() {
|
||||
quietFlag = true
|
||||
}
|
||||
|
||||
// EnableJSON - outputs logs in json format.
|
||||
func EnableJSON() {
|
||||
jsonFlag = true
|
||||
quietFlag = true
|
||||
}
|
||||
|
||||
// EnableAnonymous - turns anonymous flag
|
||||
// to avoid printing sensitive information.
|
||||
func EnableAnonymous() {
|
||||
anonFlag = true
|
||||
}
|
||||
|
||||
// IsAnonymous - returns true if anonFlag is true
|
||||
func IsAnonymous() bool {
|
||||
return anonFlag
|
||||
}
|
||||
|
||||
// IsJSON - returns true if jsonFlag is true
|
||||
func IsJSON() bool {
|
||||
return jsonFlag
|
||||
}
|
||||
|
||||
// IsQuiet - returns true if quietFlag is true
|
||||
func IsQuiet() bool {
|
||||
return quietFlag
|
||||
}
|
||||
|
||||
// RegisterError registers the specified rendering function. This latter
|
||||
// will be called for a pretty rendering of fatal errors.
|
||||
func RegisterError(f func(string, error, bool) string) {
|
||||
errorFmtFunc = f
|
||||
}
|
||||
|
||||
// Remove any duplicates and return unique entries.
|
||||
func uniqueEntries(paths []string) []string {
|
||||
m := make(set.StringSet)
|
||||
for _, p := range paths {
|
||||
if !m.Contains(p) {
|
||||
m.Add(p)
|
||||
}
|
||||
}
|
||||
return m.ToSlice()
|
||||
}
|
||||
|
||||
// Init sets the trimStrings to possible GOPATHs
|
||||
// and GOROOT directories. Also append github.com/minio/minio
|
||||
// This is done to clean up the filename, when stack trace is
|
||||
// displayed when an errors happens.
|
||||
func Init(goPath string, goRoot string) {
|
||||
var goPathList []string
|
||||
var goRootList []string
|
||||
var defaultgoPathList []string
|
||||
var defaultgoRootList []string
|
||||
pathSeperator := ":"
|
||||
// Add all possible GOPATH paths into trimStrings
|
||||
// Split GOPATH depending on the OS type
|
||||
if runtime.GOOS == "windows" {
|
||||
pathSeperator = ";"
|
||||
}
|
||||
|
||||
goPathList = strings.Split(goPath, pathSeperator)
|
||||
goRootList = strings.Split(goRoot, pathSeperator)
|
||||
defaultgoPathList = strings.Split(build.Default.GOPATH, pathSeperator)
|
||||
defaultgoRootList = strings.Split(build.Default.GOROOT, pathSeperator)
|
||||
|
||||
// Add trim string "{GOROOT}/src/" into trimStrings
|
||||
trimStrings = []string{filepath.Join(runtime.GOROOT(), "src") + string(filepath.Separator)}
|
||||
|
||||
// Add all possible path from GOPATH=path1:path2...:pathN
|
||||
// as "{path#}/src/" into trimStrings
|
||||
for _, goPathString := range goPathList {
|
||||
trimStrings = append(trimStrings, filepath.Join(goPathString, "src")+string(filepath.Separator))
|
||||
}
|
||||
|
||||
for _, goRootString := range goRootList {
|
||||
trimStrings = append(trimStrings, filepath.Join(goRootString, "src")+string(filepath.Separator))
|
||||
}
|
||||
|
||||
for _, defaultgoPathString := range defaultgoPathList {
|
||||
trimStrings = append(trimStrings, filepath.Join(defaultgoPathString, "src")+string(filepath.Separator))
|
||||
}
|
||||
|
||||
for _, defaultgoRootString := range defaultgoRootList {
|
||||
trimStrings = append(trimStrings, filepath.Join(defaultgoRootString, "src")+string(filepath.Separator))
|
||||
}
|
||||
|
||||
// Remove duplicate entries.
|
||||
trimStrings = uniqueEntries(trimStrings)
|
||||
|
||||
// Add "github.com/minio/minio" as the last to cover
|
||||
// paths like "{GOROOT}/src/github.com/minio/minio"
|
||||
// and "{GOPATH}/src/github.com/minio/minio"
|
||||
trimStrings = append(trimStrings, filepath.Join("github.com", "minio", "minio")+string(filepath.Separator))
|
||||
}
|
||||
|
||||
func trimTrace(f string) string {
|
||||
for _, trimString := range trimStrings {
|
||||
f = strings.TrimPrefix(filepath.ToSlash(f), filepath.ToSlash(trimString))
|
||||
}
|
||||
return filepath.FromSlash(f)
|
||||
}
|
||||
|
||||
func getSource(level int) string {
|
||||
pc, file, lineNumber, ok := runtime.Caller(level)
|
||||
if ok {
|
||||
// Clean up the common prefixes
|
||||
file = trimTrace(file)
|
||||
_, funcName := filepath.Split(runtime.FuncForPC(pc).Name())
|
||||
return fmt.Sprintf("%v:%v:%v()", file, lineNumber, funcName)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getTrace method - creates and returns stack trace
|
||||
func getTrace(traceLevel int) []string {
|
||||
var trace []string
|
||||
pc, file, lineNumber, ok := runtime.Caller(traceLevel)
|
||||
|
||||
for ok && file != "" {
|
||||
// Clean up the common prefixes
|
||||
file = trimTrace(file)
|
||||
// Get the function name
|
||||
_, funcName := filepath.Split(runtime.FuncForPC(pc).Name())
|
||||
// Skip duplicate traces that start with file name, "<autogenerated>"
|
||||
// and also skip traces with function name that starts with "runtime."
|
||||
if !strings.HasPrefix(file, "<autogenerated>") &&
|
||||
!strings.HasPrefix(funcName, "runtime.") {
|
||||
// Form and append a line of stack trace into a
|
||||
// collection, 'trace', to build full stack trace
|
||||
trace = append(trace, fmt.Sprintf("%v:%v:%v()", file, lineNumber, funcName))
|
||||
|
||||
// Ignore trace logs beyond the following conditions
|
||||
for _, name := range matchingFuncNames {
|
||||
if funcName == name {
|
||||
return trace
|
||||
}
|
||||
}
|
||||
}
|
||||
traceLevel++
|
||||
// Read stack trace information from PC
|
||||
pc, file, lineNumber, ok = runtime.Caller(traceLevel)
|
||||
}
|
||||
return trace
|
||||
}
|
||||
|
||||
// Return the highway hash of the passed string
|
||||
func hashString(input string) string {
|
||||
hh, _ := highwayhash.New(magicHighwayHash256Key)
|
||||
hh.Write([]byte(input))
|
||||
return hex.EncodeToString(hh.Sum(nil))
|
||||
}
|
||||
|
||||
// Kind specifies the kind of errors log
|
||||
type Kind string
|
||||
|
||||
const (
|
||||
// Minio errors
|
||||
Minio Kind = "CONSOLE"
|
||||
// Application errors
|
||||
Application Kind = "APPLICATION"
|
||||
// All errors
|
||||
All Kind = "ALL"
|
||||
)
|
||||
|
||||
// LogAlwaysIf prints a detailed errors message during
|
||||
// the execution of the server.
|
||||
func LogAlwaysIf(ctx context.Context, err error, errKind ...interface{}) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
logIf(ctx, err, errKind...)
|
||||
}
|
||||
|
||||
// LogIf prints a detailed errors message during
|
||||
// the execution of the server
|
||||
func LogIf(ctx context.Context, err error, errKind ...interface{}) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return
|
||||
}
|
||||
logIf(ctx, err, errKind...)
|
||||
}
|
||||
|
||||
// logIf prints a detailed errors message during
|
||||
// the execution of the server.
|
||||
func logIf(ctx context.Context, err error, errKind ...interface{}) {
|
||||
if Disable {
|
||||
return
|
||||
}
|
||||
logKind := string(Minio)
|
||||
if len(errKind) > 0 {
|
||||
if ek, ok := errKind[0].(Kind); ok {
|
||||
logKind = string(ek)
|
||||
}
|
||||
}
|
||||
req := GetReqInfo(ctx)
|
||||
|
||||
if req == nil {
|
||||
req = &ReqInfo{API: "SYSTEM"}
|
||||
}
|
||||
|
||||
kv := req.GetTags()
|
||||
tags := make(map[string]interface{}, len(kv))
|
||||
for _, entry := range kv {
|
||||
tags[entry.Key] = entry.Val
|
||||
}
|
||||
|
||||
// Get full stack trace
|
||||
trace := getTrace(3)
|
||||
|
||||
// Get the cause for the Error
|
||||
message := fmt.Sprintf("%v (%T)", err, err)
|
||||
if req.DeploymentID == "" {
|
||||
req.DeploymentID = GetGlobalDeploymentID()
|
||||
}
|
||||
|
||||
entry := log.Entry{
|
||||
DeploymentID: req.DeploymentID,
|
||||
Level: ErrorLvl.String(),
|
||||
LogKind: logKind,
|
||||
RemoteHost: req.RemoteHost,
|
||||
Host: req.Host,
|
||||
RequestID: req.RequestID,
|
||||
SessionID: req.SessionID,
|
||||
UserAgent: req.UserAgent,
|
||||
Time: time.Now().UTC(),
|
||||
Trace: &log.Trace{
|
||||
Message: message,
|
||||
Source: trace,
|
||||
Variables: tags,
|
||||
},
|
||||
}
|
||||
|
||||
if anonFlag {
|
||||
entry.SessionID = hashString(entry.SessionID)
|
||||
entry.RemoteHost = hashString(entry.RemoteHost)
|
||||
entry.Trace.Message = reflect.TypeOf(err).String()
|
||||
entry.Trace.Variables = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Iterate over all logger targets to send the log entry
|
||||
for _, t := range SystemTargets() {
|
||||
if err := t.Send(entry, entry.LogKind); err != nil {
|
||||
if consoleTgt != nil {
|
||||
entry.Trace.Message = fmt.Sprintf("event(%#v) was not sent to Logger target (%#v): %#v", entry, t, err)
|
||||
consoleTgt.Send(entry, entry.LogKind)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ErrCritical is the value panic'd whenever CriticalIf is called.
|
||||
var ErrCritical struct{}
|
||||
|
||||
// CriticalIf logs the provided errors on the console. It fails the
|
||||
// current go-routine by causing a `panic(ErrCritical)`.
|
||||
func CriticalIf(ctx context.Context, err error, errKind ...interface{}) {
|
||||
if err != nil {
|
||||
LogIf(ctx, err, errKind...)
|
||||
panic(ErrCritical)
|
||||
}
|
||||
}
|
||||
|
||||
// FatalIf is similar to Fatal() but it ignores passed nil errors
|
||||
func FatalIf(err error, msg string, data ...interface{}) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
fatal(err, msg, data...)
|
||||
}
|
||||
|
||||
func applyDynamicConfigForSubSys(ctx context.Context, transport *http.Transport, subSys string) error {
|
||||
switch subSys {
|
||||
case config.LoggerWebhookSubSys:
|
||||
loggerCfg, err := LookupConfigForSubSys(config.LoggerWebhookSubSys)
|
||||
if err != nil {
|
||||
LogIf(ctx, fmt.Errorf("unable to load logger webhook config: %w", err))
|
||||
return err
|
||||
}
|
||||
userAgent := getUserAgent()
|
||||
for n, l := range loggerCfg.HTTP {
|
||||
if l.Enabled {
|
||||
l.LogOnce = LogOnceIf
|
||||
l.UserAgent = userAgent
|
||||
l.Transport = NewHTTPTransportWithClientCerts(transport, l.ClientCert, l.ClientKey)
|
||||
loggerCfg.HTTP[n] = l
|
||||
}
|
||||
}
|
||||
err = UpdateSystemTargets(loggerCfg)
|
||||
if err != nil {
|
||||
LogIf(ctx, fmt.Errorf("unable to update logger webhook config: %w", err))
|
||||
return err
|
||||
}
|
||||
case config.AuditWebhookSubSys:
|
||||
loggerCfg, err := LookupConfigForSubSys(config.AuditWebhookSubSys)
|
||||
if err != nil {
|
||||
LogIf(ctx, fmt.Errorf("unable to load audit webhook config: %w", err))
|
||||
return err
|
||||
}
|
||||
userAgent := getUserAgent()
|
||||
for n, l := range loggerCfg.AuditWebhook {
|
||||
if l.Enabled {
|
||||
l.LogOnce = LogOnceIf
|
||||
l.UserAgent = userAgent
|
||||
l.Transport = NewHTTPTransportWithClientCerts(transport, l.ClientCert, l.ClientKey)
|
||||
loggerCfg.AuditWebhook[n] = l
|
||||
}
|
||||
}
|
||||
|
||||
err = UpdateAuditWebhookTargets(loggerCfg)
|
||||
if err != nil {
|
||||
LogIf(ctx, fmt.Errorf("Unable to update audit webhook targets: %w", err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitializeLogger :
|
||||
func InitializeLogger(ctx context.Context, transport *http.Transport) error {
|
||||
err := applyDynamicConfigForSubSys(ctx, transport, config.LoggerWebhookSubSys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = applyDynamicConfigForSubSys(ctx, transport, config.AuditWebhookSubSys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if enable, _ := config.ParseBool(env.Get(EnvLoggerJSONEnable, "")); enable {
|
||||
EnableJSON()
|
||||
}
|
||||
if enable, _ := config.ParseBool(env.Get(EnvLoggerAnonymousEnable, "")); enable {
|
||||
EnableAnonymous()
|
||||
}
|
||||
if enable, _ := config.ParseBool(env.Get(EnvLoggerQuietEnable, "")); enable {
|
||||
EnableQuiet()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getUserAgent() string {
|
||||
userAgentParts := []string{}
|
||||
// Helper function to concisely append a pair of strings to a
|
||||
// the user-agent slice.
|
||||
uaAppend := func(p, q string) {
|
||||
userAgentParts = append(userAgentParts, p, q)
|
||||
}
|
||||
uaAppend("Console (", runtime.GOOS)
|
||||
uaAppend("; ", runtime.GOARCH)
|
||||
uaAppend(") Console/", pkg.Version)
|
||||
uaAppend(" Console/", pkg.ReleaseTag)
|
||||
uaAppend(" Console/", pkg.CommitID)
|
||||
|
||||
return strings.Join(userAgentParts, "")
|
||||
}
|
||||
|
||||
// NewHTTPTransportWithClientCerts returns a new http configuration
|
||||
// used while communicating with the cloud backends.
|
||||
func NewHTTPTransportWithClientCerts(parentTransport *http.Transport, clientCert, clientKey string) *http.Transport {
|
||||
transport := parentTransport.Clone()
|
||||
if clientCert != "" && clientKey != "" {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
c, err := certs.NewManager(ctx, clientCert, clientKey, tls.LoadX509KeyPair)
|
||||
if err != nil {
|
||||
LogIf(ctx, fmt.Errorf("failed to load client key and cert, please check your endpoint configuration: %s",
|
||||
err.Error()))
|
||||
}
|
||||
if c != nil {
|
||||
c.UpdateReloadDuration(10 * time.Second)
|
||||
c.ReloadOnSignal(syscall.SIGHUP) // allow reloads upon SIGHUP
|
||||
transport.TLSClientConfig.GetClientCertificate = c.GetClientCertificate
|
||||
}
|
||||
}
|
||||
return transport
|
||||
}
|
||||
243
pkg/logger/logger_test.go
Normal file
243
pkg/logger/logger_test.go
Normal file
@@ -0,0 +1,243 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testServer(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func TestInitializeLogger(t *testing.T) {
|
||||
testServerWillStart := make(chan interface{})
|
||||
http.HandleFunc("/", testServer)
|
||||
go func() {
|
||||
close(testServerWillStart)
|
||||
err := http.ListenAndServe("127.0.0.1:1337", nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
<-testServerWillStart
|
||||
|
||||
loggerWebhookEnable := fmt.Sprintf("%s_TEST", EnvLoggerWebhookEnable)
|
||||
loggerWebhookEndpoint := fmt.Sprintf("%s_TEST", EnvLoggerWebhookEndpoint)
|
||||
loggerWebhookAuthToken := fmt.Sprintf("%s_TEST", EnvLoggerWebhookAuthToken)
|
||||
loggerWebhookClientCert := fmt.Sprintf("%s_TEST", EnvLoggerWebhookClientCert)
|
||||
loggerWebhookClientKey := fmt.Sprintf("%s_TEST", EnvLoggerWebhookClientKey)
|
||||
loggerWebhookQueueSize := fmt.Sprintf("%s_TEST", EnvLoggerWebhookQueueSize)
|
||||
|
||||
auditWebhookEnable := fmt.Sprintf("%s_TEST", EnvAuditWebhookEnable)
|
||||
auditWebhookEndpoint := fmt.Sprintf("%s_TEST", EnvAuditWebhookEndpoint)
|
||||
auditWebhookAuthToken := fmt.Sprintf("%s_TEST", EnvAuditWebhookAuthToken)
|
||||
auditWebhookClientCert := fmt.Sprintf("%s_TEST", EnvAuditWebhookClientCert)
|
||||
auditWebhookClientKey := fmt.Sprintf("%s_TEST", EnvAuditWebhookClientKey)
|
||||
auditWebhookQueueSize := fmt.Sprintf("%s_TEST", EnvAuditWebhookQueueSize)
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
transport *http.Transport
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
setEnvVars func()
|
||||
unsetEnvVars func()
|
||||
}{
|
||||
{
|
||||
name: "logger or auditlog is not enabled",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
transport: http.DefaultTransport.(*http.Transport).Clone(),
|
||||
},
|
||||
wantErr: false,
|
||||
setEnvVars: func() {
|
||||
},
|
||||
unsetEnvVars: func() {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "logger webhook initialized correctly",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
transport: http.DefaultTransport.(*http.Transport).Clone(),
|
||||
},
|
||||
wantErr: false,
|
||||
setEnvVars: func() {
|
||||
os.Setenv(loggerWebhookEnable, "on")
|
||||
os.Setenv(loggerWebhookEndpoint, "http://127.0.0.1:1337/logger")
|
||||
os.Setenv(loggerWebhookAuthToken, "test")
|
||||
os.Setenv(loggerWebhookClientCert, "")
|
||||
os.Setenv(loggerWebhookClientKey, "")
|
||||
os.Setenv(loggerWebhookQueueSize, "1000")
|
||||
},
|
||||
unsetEnvVars: func() {
|
||||
os.Unsetenv(loggerWebhookEnable)
|
||||
os.Unsetenv(loggerWebhookEndpoint)
|
||||
os.Unsetenv(loggerWebhookAuthToken)
|
||||
os.Unsetenv(loggerWebhookClientCert)
|
||||
os.Unsetenv(loggerWebhookClientKey)
|
||||
os.Unsetenv(loggerWebhookQueueSize)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "logger webhook failed to initialize",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
transport: http.DefaultTransport.(*http.Transport).Clone(),
|
||||
},
|
||||
wantErr: true,
|
||||
setEnvVars: func() {
|
||||
os.Setenv(loggerWebhookEnable, "on")
|
||||
os.Setenv(loggerWebhookEndpoint, "https://aklsjdakljdjkalsd.com")
|
||||
os.Setenv(loggerWebhookAuthToken, "test")
|
||||
os.Setenv(loggerWebhookClientCert, "")
|
||||
os.Setenv(loggerWebhookClientKey, "")
|
||||
os.Setenv(loggerWebhookQueueSize, "1000")
|
||||
},
|
||||
unsetEnvVars: func() {
|
||||
os.Unsetenv(loggerWebhookEnable)
|
||||
os.Unsetenv(loggerWebhookEndpoint)
|
||||
os.Unsetenv(loggerWebhookAuthToken)
|
||||
os.Unsetenv(loggerWebhookClientCert)
|
||||
os.Unsetenv(loggerWebhookClientKey)
|
||||
os.Unsetenv(loggerWebhookQueueSize)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "auditlog webhook initialized correctly",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
transport: http.DefaultTransport.(*http.Transport).Clone(),
|
||||
},
|
||||
wantErr: false,
|
||||
setEnvVars: func() {
|
||||
os.Setenv(auditWebhookEnable, "on")
|
||||
os.Setenv(auditWebhookEndpoint, "http://127.0.0.1:1337/audit")
|
||||
os.Setenv(auditWebhookAuthToken, "test")
|
||||
os.Setenv(auditWebhookClientCert, "")
|
||||
os.Setenv(auditWebhookClientKey, "")
|
||||
os.Setenv(auditWebhookQueueSize, "1000")
|
||||
},
|
||||
unsetEnvVars: func() {
|
||||
os.Unsetenv(auditWebhookEnable)
|
||||
os.Unsetenv(auditWebhookEndpoint)
|
||||
os.Unsetenv(auditWebhookAuthToken)
|
||||
os.Unsetenv(auditWebhookClientCert)
|
||||
os.Unsetenv(auditWebhookClientKey)
|
||||
os.Unsetenv(auditWebhookQueueSize)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "auditlog webhook failed to initialize",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
transport: http.DefaultTransport.(*http.Transport).Clone(),
|
||||
},
|
||||
wantErr: true,
|
||||
setEnvVars: func() {
|
||||
os.Setenv(auditWebhookEnable, "on")
|
||||
os.Setenv(auditWebhookEndpoint, "https://aklsjdakljdjkalsd.com")
|
||||
os.Setenv(auditWebhookAuthToken, "test")
|
||||
os.Setenv(auditWebhookClientCert, "")
|
||||
os.Setenv(auditWebhookClientKey, "")
|
||||
os.Setenv(auditWebhookQueueSize, "1000")
|
||||
},
|
||||
unsetEnvVars: func() {
|
||||
os.Unsetenv(auditWebhookEnable)
|
||||
os.Unsetenv(auditWebhookEndpoint)
|
||||
os.Unsetenv(auditWebhookAuthToken)
|
||||
os.Unsetenv(auditWebhookClientCert)
|
||||
os.Unsetenv(auditWebhookClientKey)
|
||||
os.Unsetenv(auditWebhookQueueSize)
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.setEnvVars != nil {
|
||||
tt.setEnvVars()
|
||||
}
|
||||
if err := InitializeLogger(tt.args.ctx, tt.args.transport); (err != nil) != tt.wantErr {
|
||||
t.Errorf("InitializeLogger() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if tt.unsetEnvVars != nil {
|
||||
tt.unsetEnvVars()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
}{
|
||||
{
|
||||
name: "enable json",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
EnableJSON()
|
||||
if !IsJSON() {
|
||||
t.Errorf("EnableJSON() = %v, want %v", IsJSON(), true)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableQuiet(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
}{
|
||||
{
|
||||
name: "enable quiet",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
EnableQuiet()
|
||||
if !IsQuiet() {
|
||||
t.Errorf("EnableQuiet() = %v, want %v", IsQuiet(), true)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableAnonymous(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
}{
|
||||
{
|
||||
name: "enable anonymous",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
EnableAnonymous()
|
||||
if !IsAnonymous() {
|
||||
t.Errorf("EnableAnonymous() = %v, want %v", IsAnonymous(), true)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
92
pkg/logger/logonce.go
Normal file
92
pkg/logger/logonce.go
Normal file
@@ -0,0 +1,92 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Holds a map of recently logged errors.
|
||||
type logOnceType struct {
|
||||
IDMap map[interface{}]error
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
// One log message per errors.
|
||||
func (l *logOnceType) logOnceIf(ctx context.Context, err error, id interface{}, errKind ...interface{}) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
l.Lock()
|
||||
shouldLog := false
|
||||
prevErr := l.IDMap[id]
|
||||
if prevErr == nil {
|
||||
l.IDMap[id] = err
|
||||
shouldLog = true
|
||||
} else if prevErr.Error() != err.Error() {
|
||||
l.IDMap[id] = err
|
||||
shouldLog = true
|
||||
}
|
||||
l.Unlock()
|
||||
|
||||
if shouldLog {
|
||||
LogIf(ctx, err, errKind...)
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup the map every 30 minutes so that the log message is printed again for the user to notice.
|
||||
func (l *logOnceType) cleanupRoutine() {
|
||||
for {
|
||||
l.Lock()
|
||||
l.IDMap = make(map[interface{}]error)
|
||||
l.Unlock()
|
||||
|
||||
time.Sleep(30 * time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns logOnceType
|
||||
func newLogOnceType() *logOnceType {
|
||||
l := &logOnceType{IDMap: make(map[interface{}]error)}
|
||||
go l.cleanupRoutine()
|
||||
return l
|
||||
}
|
||||
|
||||
var logOnce = newLogOnceType()
|
||||
|
||||
// LogOnceIf - Logs notification errors - once per errors.
|
||||
// id is a unique identifier for related log messages, refer to cmd/notification.go
|
||||
// on how it is used.
|
||||
func LogOnceIf(ctx context.Context, err error, id interface{}, errKind ...interface{}) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return
|
||||
}
|
||||
|
||||
if err.Error() == http.ErrServerClosed.Error() || err.Error() == "disk not found" {
|
||||
return
|
||||
}
|
||||
|
||||
logOnce.logOnceIf(ctx, err, id, errKind...)
|
||||
}
|
||||
130
pkg/logger/message/audit/entry.go
Normal file
130
pkg/logger/message/audit/entry.go
Normal file
@@ -0,0 +1,130 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package audit
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
|
||||
"github.com/minio/console/pkg/utils"
|
||||
|
||||
xhttp "github.com/minio/console/pkg/http"
|
||||
)
|
||||
|
||||
// Version - represents the current version of audit log structure.
|
||||
const Version = "1"
|
||||
|
||||
// ObjectVersion object version key/versionId
|
||||
type ObjectVersion struct {
|
||||
ObjectName string `json:"objectName"`
|
||||
VersionID string `json:"versionId,omitempty"`
|
||||
}
|
||||
|
||||
// Entry - audit entry logs.
|
||||
type Entry struct {
|
||||
Version string `json:"version"`
|
||||
DeploymentID string `json:"deploymentid,omitempty"`
|
||||
Time time.Time `json:"time"`
|
||||
Trigger string `json:"trigger"`
|
||||
API struct {
|
||||
Path string `json:"path,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Method string `json:"method"`
|
||||
StatusCode int `json:"statusCode,omitempty"`
|
||||
InputBytes int64 `json:"rx"`
|
||||
OutputBytes int64 `json:"tx"`
|
||||
TimeToFirstByte string `json:"timeToFirstByte,omitempty"`
|
||||
TimeToResponse string `json:"timeToResponse,omitempty"`
|
||||
} `json:"api"`
|
||||
RemoteHost string `json:"remotehost,omitempty"`
|
||||
RequestID string `json:"requestID,omitempty"`
|
||||
SessionID string `json:"sessionID,omitempty"`
|
||||
UserAgent string `json:"userAgent,omitempty"`
|
||||
ReqClaims map[string]interface{} `json:"requestClaims,omitempty"`
|
||||
ReqQuery map[string]string `json:"requestQuery,omitempty"`
|
||||
ReqHeader map[string]string `json:"requestHeader,omitempty"`
|
||||
RespHeader map[string]string `json:"responseHeader,omitempty"`
|
||||
Tags map[string]interface{} `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// NewEntry - constructs an audit entry object with some fields filled
|
||||
func NewEntry(deploymentID string) Entry {
|
||||
return Entry{
|
||||
Version: Version,
|
||||
DeploymentID: deploymentID,
|
||||
Time: time.Now().UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
// ToEntry - constructs an audit entry from a http request
|
||||
func ToEntry(w http.ResponseWriter, r *http.Request, reqClaims map[string]interface{}, deploymentID string) Entry {
|
||||
entry := NewEntry(deploymentID)
|
||||
|
||||
entry.RemoteHost = r.RemoteAddr
|
||||
entry.UserAgent = r.UserAgent()
|
||||
entry.ReqClaims = reqClaims
|
||||
|
||||
q := r.URL.Query()
|
||||
reqQuery := make(map[string]string, len(q))
|
||||
for k, v := range q {
|
||||
reqQuery[k] = strings.Join(v, ",")
|
||||
}
|
||||
entry.ReqQuery = reqQuery
|
||||
|
||||
reqHeader := make(map[string]string, len(r.Header))
|
||||
for k, v := range r.Header {
|
||||
reqHeader[k] = strings.Join(v, ",")
|
||||
}
|
||||
entry.ReqHeader = reqHeader
|
||||
|
||||
wh := w.Header()
|
||||
|
||||
var requestID interface{}
|
||||
requestID = r.Context().Value(utils.ContextRequestID)
|
||||
if requestID == nil {
|
||||
requestID, _ = utils.NewUUID()
|
||||
}
|
||||
entry.RequestID = requestID.(string)
|
||||
|
||||
if val := r.Context().Value(utils.ContextRequestUserID); val != nil {
|
||||
sessionID := val.(string)
|
||||
if os.Getenv("CONSOLE_OPERATOR_MODE") != "" && os.Getenv("CONSOLE_OPERATOR_MODE") == "on" {
|
||||
claims := jwt.MapClaims{}
|
||||
_, _ = jwt.ParseWithClaims(sessionID, claims, nil)
|
||||
if sub, ok := claims["sub"]; ok {
|
||||
sessionID = sub.(string)
|
||||
}
|
||||
}
|
||||
entry.SessionID = sessionID
|
||||
}
|
||||
|
||||
respHeader := make(map[string]string, len(wh))
|
||||
for k, v := range wh {
|
||||
respHeader[k] = strings.Join(v, ",")
|
||||
}
|
||||
entry.RespHeader = respHeader
|
||||
|
||||
if etag := respHeader[xhttp.ETag]; etag != "" {
|
||||
respHeader[xhttp.ETag] = strings.Trim(etag, `"`)
|
||||
}
|
||||
|
||||
return entry
|
||||
}
|
||||
126
pkg/logger/message/audit/entry_test.go
Normal file
126
pkg/logger/message/audit/entry_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/console/pkg/utils"
|
||||
)
|
||||
|
||||
func TestNewEntry(t *testing.T) {
|
||||
type args struct {
|
||||
deploymentID string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want Entry
|
||||
}{
|
||||
{
|
||||
name: "constructs an audit entry object with some fields filled",
|
||||
args: args{
|
||||
deploymentID: "1",
|
||||
},
|
||||
want: Entry{
|
||||
Version: Version,
|
||||
DeploymentID: "1",
|
||||
Time: time.Now().UTC(),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := NewEntry(tt.args.deploymentID); got.DeploymentID != tt.want.DeploymentID {
|
||||
t.Errorf("NewEntry() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToEntry(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/tenants?test=xyz", nil)
|
||||
req.Header.Set("Authorization", "xyz")
|
||||
req.Header.Set("ETag", "\"ABCDE\"")
|
||||
|
||||
// applying context information
|
||||
ctx := context.WithValue(req.Context(), utils.ContextRequestUserID, "eyJhbGciOiJSUzI1NiIsImtpZCI6Ing5cS0wSkEwQzFMWDJlRlR3dHo2b0t0NVNnRzJad0llMGVNczMxbjU0b2sifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJtaW5pby1vcGVyYXRvciIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJjb25zb2xlLXNhLXRva2VuLWJrZzZwIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImNvbnNvbGUtc2EiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiJhZTE2ZGVkNS01MmM3LTRkZTQtOWUxYS1iNmI4NGU2OGMzM2UiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6bWluaW8tb3BlcmF0b3I6Y29uc29sZS1zYSJ9.AjhzekAPC59SQVBQL5sr-1dqr57-jH8a5LVazpnEr_cC0JqT4jXYjdfbrZSF9yaL4gHRv2l0kOhBlrjRK7y-IpMbxE71Fne_lSzaptSuqgI5I9dFvpVfZWP1yMAqav8mrlUoWkWDq9IAkyH4bvvZrVgQJGgd5t9U_7DQCVwbkQvy0wGS5zoMcZhYenn_Ub1BoxWcviADQ1aY1wQju8OP0IOwKTIMXMQqciOFdJ9T5-tQEGUrikTu_tW-1shUHzOxBcEzGVtBvBy2OmbNnRFYogbhmp-Dze6EAi035bY32bfL7XKBUNCW6_3VbN_h3pQNAuT2NJOSKuhJ3cGldCB2zg")
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
w.Header().Set("Authorization", "xyz")
|
||||
w.Header().Set("ETag", "\"ABCDE\"")
|
||||
|
||||
type args struct {
|
||||
w http.ResponseWriter
|
||||
r *http.Request
|
||||
reqClaims map[string]interface{}
|
||||
deploymentID string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want Entry
|
||||
preFunc func()
|
||||
postFunc func()
|
||||
}{
|
||||
{
|
||||
preFunc: func() {
|
||||
os.Setenv("CONSOLE_OPERATOR_MODE", "on")
|
||||
},
|
||||
postFunc: func() {
|
||||
os.Unsetenv("CONSOLE_OPERATOR_MODE")
|
||||
|
||||
},
|
||||
name: "constructs an audit entry from a http request",
|
||||
args: args{
|
||||
w: w,
|
||||
r: req,
|
||||
reqClaims: map[string]interface{}{},
|
||||
deploymentID: "1",
|
||||
},
|
||||
want: Entry{
|
||||
Version: "1",
|
||||
DeploymentID: "1",
|
||||
SessionID: "system:serviceaccount:minio-operator:console-sa",
|
||||
ReqQuery: map[string]string{"test": "xyz"},
|
||||
ReqHeader: map[string]string{"test": "xyz"},
|
||||
RespHeader: map[string]string{"test": "xyz", "ETag": "ABCDE"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.preFunc != nil {
|
||||
tt.preFunc()
|
||||
}
|
||||
if got := ToEntry(tt.args.w, tt.args.r, tt.args.reqClaims, tt.args.deploymentID); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ToEntry() = %v, want %v", got, tt.want)
|
||||
}
|
||||
if tt.postFunc != nil {
|
||||
tt.postFunc()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
64
pkg/logger/message/log/entry.go
Normal file
64
pkg/logger/message/log/entry.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ObjectVersion object version key/versionId
|
||||
type ObjectVersion struct {
|
||||
ObjectName string `json:"objectName"`
|
||||
VersionID string `json:"versionId,omitempty"`
|
||||
}
|
||||
|
||||
// Args - defines the arguments for the API.
|
||||
type Args struct {
|
||||
Bucket string `json:"bucket,omitempty"`
|
||||
Object string `json:"object,omitempty"`
|
||||
VersionID string `json:"versionId,omitempty"`
|
||||
Objects []ObjectVersion `json:"objects,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// Trace - defines the trace.
|
||||
type Trace struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
Source []string `json:"source,omitempty"`
|
||||
Variables map[string]interface{} `json:"variables,omitempty"`
|
||||
}
|
||||
|
||||
// API - defines the api type and its args.
|
||||
type API struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
// Entry - defines fields and values of each log entry.
|
||||
type Entry struct {
|
||||
DeploymentID string `json:"deploymentid,omitempty"`
|
||||
Level string `json:"level"`
|
||||
LogKind string `json:"errKind"`
|
||||
Time time.Time `json:"time"`
|
||||
API *API `json:"api,omitempty"`
|
||||
RemoteHost string `json:"remotehost,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
RequestID string `json:"requestID,omitempty"`
|
||||
SessionID string `json:"sessionID,omitempty"`
|
||||
UserAgent string `json:"userAgent,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Trace *Trace `json:"errors,omitempty"`
|
||||
}
|
||||
117
pkg/logger/reqinfo.go
Normal file
117
pkg/logger/reqinfo.go
Normal file
@@ -0,0 +1,117 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/minio/console/pkg/utils"
|
||||
)
|
||||
|
||||
// KeyVal - appended to ReqInfo.Tags
|
||||
type KeyVal struct {
|
||||
Key string
|
||||
Val interface{}
|
||||
}
|
||||
|
||||
// ObjectVersion object version key/versionId
|
||||
type ObjectVersion struct {
|
||||
ObjectName string
|
||||
VersionID string `json:"VersionId,omitempty"`
|
||||
}
|
||||
|
||||
// ReqInfo stores the request info.
|
||||
type ReqInfo struct {
|
||||
RemoteHost string // Client Host/IP
|
||||
Host string // Node Host/IP
|
||||
UserAgent string // User Agent
|
||||
DeploymentID string // x-minio-deployment-id
|
||||
RequestID string // x-amz-request-id
|
||||
SessionID string // custom session id
|
||||
API string // API name - GetObject PutObject NewMultipartUpload etc.
|
||||
BucketName string `json:",omitempty"` // Bucket name
|
||||
ObjectName string `json:",omitempty"` // Object name
|
||||
VersionID string `json:",omitempty"` // corresponding versionID for the object
|
||||
Objects []ObjectVersion `json:",omitempty"` // Only set during MultiObject delete handler.
|
||||
AccessKey string // Access Key
|
||||
tags []KeyVal // Any additional info not accommodated by above fields
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// GetTags - returns the user defined tags
|
||||
func (r *ReqInfo) GetTags() []KeyVal {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
return append([]KeyVal(nil), r.tags...)
|
||||
}
|
||||
|
||||
// GetTagsMap - returns the user defined tags in a map structure
|
||||
func (r *ReqInfo) GetTagsMap() map[string]interface{} {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
m := make(map[string]interface{}, len(r.tags))
|
||||
for _, t := range r.tags {
|
||||
m[t.Key] = t.Val
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// SetReqInfo sets ReqInfo in the context.
|
||||
func SetReqInfo(ctx context.Context, req *ReqInfo) context.Context {
|
||||
if ctx == nil {
|
||||
LogIf(context.Background(), fmt.Errorf("context is nil"))
|
||||
return nil
|
||||
}
|
||||
return context.WithValue(ctx, utils.ContextLogKey, req)
|
||||
}
|
||||
|
||||
// GetReqInfo returns ReqInfo if set.
|
||||
func GetReqInfo(ctx context.Context) *ReqInfo {
|
||||
if ctx != nil {
|
||||
r, ok := ctx.Value(utils.ContextLogKey).(*ReqInfo)
|
||||
if ok {
|
||||
return r
|
||||
}
|
||||
r = &ReqInfo{}
|
||||
if val, o := ctx.Value(utils.ContextRequestID).(string); o {
|
||||
r.RequestID = val
|
||||
}
|
||||
if val, o := ctx.Value(utils.ContextRequestUserID).(string); o {
|
||||
r.SessionID = val
|
||||
}
|
||||
if val, o := ctx.Value(utils.ContextRequestUserAgent).(string); o {
|
||||
r.UserAgent = val
|
||||
}
|
||||
if val, o := ctx.Value(utils.ContextRequestHost).(string); o {
|
||||
r.Host = val
|
||||
}
|
||||
if val, o := ctx.Value(utils.ContextRequestRemoteAddr).(string); o {
|
||||
r.RemoteHost = val
|
||||
}
|
||||
SetReqInfo(ctx, r)
|
||||
return r
|
||||
}
|
||||
return nil
|
||||
}
|
||||
226
pkg/logger/target/http/http.go
Normal file
226
pkg/logger/target/http/http.go
Normal file
@@ -0,0 +1,226 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
xhttp "github.com/minio/console/pkg/http"
|
||||
"github.com/minio/console/pkg/logger/target/types"
|
||||
)
|
||||
|
||||
// Timeout for the webhook http call
|
||||
const webhookCallTimeout = 5 * time.Second
|
||||
|
||||
// Config http logger target
|
||||
type Config struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Name string `json:"name"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
AuthToken string `json:"authToken"`
|
||||
ClientCert string `json:"clientCert"`
|
||||
ClientKey string `json:"clientKey"`
|
||||
QueueSize int `json:"queueSize"`
|
||||
Transport http.RoundTripper `json:"-"`
|
||||
|
||||
// Custom logger
|
||||
LogOnce func(ctx context.Context, err error, id interface{}, errKind ...interface{}) `json:"-"`
|
||||
}
|
||||
|
||||
// Target implements logger.Target and sends the json
|
||||
// format of a log entry to the configured http endpoint.
|
||||
// An internal buffer of logs is maintained but when the
|
||||
// buffer is full, new logs are just ignored and an errors
|
||||
// is returned to the caller.
|
||||
type Target struct {
|
||||
status int32
|
||||
wg sync.WaitGroup
|
||||
|
||||
// Channel of log entries
|
||||
logCh chan interface{}
|
||||
|
||||
config Config
|
||||
}
|
||||
|
||||
// Endpoint returns the backend endpoint
|
||||
func (h *Target) Endpoint() string {
|
||||
return h.config.Endpoint
|
||||
}
|
||||
|
||||
func (h *Target) String() string {
|
||||
return h.config.Name
|
||||
}
|
||||
|
||||
// Init validate and initialize the http target
|
||||
func (h *Target) Init() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*webhookCallTimeout)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, h.config.Endpoint, strings.NewReader(`{}`))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set(xhttp.ContentType, "application/json")
|
||||
|
||||
// Set user-agent to indicate MinIO release
|
||||
// version to the configured log endpoint
|
||||
req.Header.Set("User-Agent", h.config.UserAgent)
|
||||
|
||||
if h.config.AuthToken != "" {
|
||||
req.Header.Set("Authorization", h.config.AuthToken)
|
||||
}
|
||||
|
||||
client := http.Client{Transport: h.config.Transport}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Drain any response.
|
||||
xhttp.DrainBody(resp.Body)
|
||||
|
||||
if !acceptedResponseStatusCode(resp.StatusCode) {
|
||||
switch resp.StatusCode {
|
||||
case http.StatusForbidden:
|
||||
return fmt.Errorf("%s returned '%s', please check if your auth token is correctly set",
|
||||
h.config.Endpoint, resp.Status)
|
||||
}
|
||||
return fmt.Errorf("%s returned '%s', please check your endpoint configuration",
|
||||
h.config.Endpoint, resp.Status)
|
||||
}
|
||||
|
||||
h.status = 1
|
||||
go h.startHTTPLogger()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Accepted HTTP Status Codes
|
||||
var acceptedStatusCodeMap = map[int]bool{http.StatusOK: true, http.StatusCreated: true, http.StatusAccepted: true, http.StatusNoContent: true}
|
||||
|
||||
func acceptedResponseStatusCode(code int) bool {
|
||||
return acceptedStatusCodeMap[code]
|
||||
}
|
||||
|
||||
func (h *Target) logEntry(entry interface{}) {
|
||||
logJSON, err := json.Marshal(&entry)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), webhookCallTimeout)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
||||
h.config.Endpoint, bytes.NewReader(logJSON))
|
||||
if err != nil {
|
||||
h.config.LogOnce(ctx, fmt.Errorf("%s returned '%w', please check your endpoint configuration", h.config.Endpoint, err), h.config.Endpoint)
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
req.Header.Set(xhttp.ContentType, "application/json")
|
||||
|
||||
// Set user-agent to indicate MinIO release
|
||||
// version to the configured log endpoint
|
||||
req.Header.Set("User-Agent", h.config.UserAgent)
|
||||
|
||||
if h.config.AuthToken != "" {
|
||||
req.Header.Set("Authorization", h.config.AuthToken)
|
||||
}
|
||||
|
||||
client := http.Client{Transport: h.config.Transport}
|
||||
resp, err := client.Do(req)
|
||||
cancel()
|
||||
if err != nil {
|
||||
h.config.LogOnce(ctx, fmt.Errorf("%s returned '%w', please check your endpoint configuration", h.config.Endpoint, err), h.config.Endpoint)
|
||||
return
|
||||
}
|
||||
|
||||
// Drain any response.
|
||||
xhttp.DrainBody(resp.Body)
|
||||
|
||||
if !acceptedResponseStatusCode(resp.StatusCode) {
|
||||
switch resp.StatusCode {
|
||||
case http.StatusForbidden:
|
||||
h.config.LogOnce(ctx, fmt.Errorf("%s returned '%s', please check if your auth token is correctly set", h.config.Endpoint, resp.Status), h.config.Endpoint)
|
||||
default:
|
||||
h.config.LogOnce(ctx, fmt.Errorf("%s returned '%s', please check your endpoint configuration", h.config.Endpoint, resp.Status), h.config.Endpoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Target) startHTTPLogger() {
|
||||
// Create a routine which sends json logs received
|
||||
// from an internal channel.
|
||||
go func() {
|
||||
h.wg.Add(1)
|
||||
defer h.wg.Done()
|
||||
for entry := range h.logCh {
|
||||
h.logEntry(entry)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// New initializes a new logger target which
|
||||
// sends log over http to the specified endpoint
|
||||
func New(config Config) *Target {
|
||||
h := &Target{
|
||||
logCh: make(chan interface{}, config.QueueSize),
|
||||
config: config,
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// Send log message 'e' to http target.
|
||||
func (h *Target) Send(entry interface{}, errKind string) error {
|
||||
if atomic.LoadInt32(&h.status) == 0 {
|
||||
// Channel was closed or used before init.
|
||||
return nil
|
||||
}
|
||||
|
||||
select {
|
||||
case h.logCh <- entry:
|
||||
default:
|
||||
// log channel is full, do not wait and return
|
||||
// an errors immediately to the caller
|
||||
return errors.New("log buffer full")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cancel - cancels the target
|
||||
func (h *Target) Cancel() {
|
||||
if atomic.CompareAndSwapInt32(&h.status, 1, 0) {
|
||||
close(h.logCh)
|
||||
}
|
||||
h.wg.Wait()
|
||||
}
|
||||
|
||||
// Type - returns type of the target
|
||||
func (h *Target) Type() types.TargetType {
|
||||
return types.TargetHTTP
|
||||
}
|
||||
27
pkg/logger/target/types/types.go
Normal file
27
pkg/logger/target/types/types.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package types
|
||||
|
||||
// TargetType indicates type of the target e.g. console, http, kafka
|
||||
type TargetType uint8
|
||||
|
||||
// Constants for target types
|
||||
const (
|
||||
_ TargetType = iota
|
||||
TargetConsole
|
||||
TargetHTTP
|
||||
)
|
||||
151
pkg/logger/targets.go
Normal file
151
pkg/logger/targets.go
Normal file
@@ -0,0 +1,151 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package logger
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/minio/console/pkg/logger/target/http"
|
||||
"github.com/minio/console/pkg/logger/target/types"
|
||||
)
|
||||
|
||||
// Target is the entity that we will receive
|
||||
// a single log entry and Send it to the log target
|
||||
// e.g. Send the log to a http server
|
||||
type Target interface {
|
||||
String() string
|
||||
Endpoint() string
|
||||
Init() error
|
||||
Cancel()
|
||||
Send(entry interface{}, errKind string) error
|
||||
Type() types.TargetType
|
||||
}
|
||||
|
||||
var (
|
||||
// swapMu must be held while reading slice info or swapping targets or auditTargets.
|
||||
swapMu sync.Mutex
|
||||
|
||||
// systemTargets is the set of enabled loggers.
|
||||
// Must be immutable at all times.
|
||||
// Can be swapped to another while holding swapMu
|
||||
systemTargets = []Target{}
|
||||
|
||||
// This is always set represent /dev/console target
|
||||
consoleTgt Target
|
||||
|
||||
nTargets int32 // atomic count of len(targets)
|
||||
)
|
||||
|
||||
// SystemTargets returns active targets.
|
||||
// Returned slice may not be modified in any way.
|
||||
func SystemTargets() []Target {
|
||||
if atomic.LoadInt32(&nTargets) == 0 {
|
||||
// Lock free if none...
|
||||
return nil
|
||||
}
|
||||
swapMu.Lock()
|
||||
res := systemTargets
|
||||
swapMu.Unlock()
|
||||
return res
|
||||
}
|
||||
|
||||
// AuditTargets returns active audit targets.
|
||||
// Returned slice may not be modified in any way.
|
||||
func AuditTargets() []Target {
|
||||
if atomic.LoadInt32(&nAuditTargets) == 0 {
|
||||
// Lock free if none...
|
||||
return nil
|
||||
}
|
||||
swapMu.Lock()
|
||||
res := auditTargets
|
||||
swapMu.Unlock()
|
||||
return res
|
||||
}
|
||||
|
||||
// auditTargets is the list of enabled audit loggers
|
||||
// Must be immutable at all times.
|
||||
// Can be swapped to another while holding swapMu
|
||||
var (
|
||||
auditTargets = []Target{}
|
||||
nAuditTargets int32 // atomic count of len(auditTargets)
|
||||
)
|
||||
|
||||
func cancelAllSystemTargets() {
|
||||
for _, tgt := range systemTargets {
|
||||
tgt.Cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func initSystemTargets(cfgMap map[string]http.Config) (tgts []Target, err error) {
|
||||
for _, l := range cfgMap {
|
||||
if l.Enabled {
|
||||
t := http.New(l)
|
||||
if err = t.Init(); err != nil {
|
||||
return tgts, err
|
||||
}
|
||||
tgts = append(tgts, t)
|
||||
}
|
||||
}
|
||||
return tgts, err
|
||||
}
|
||||
|
||||
// UpdateSystemTargets swaps targets with newly loaded ones from the cfg
|
||||
func UpdateSystemTargets(cfg Config) error {
|
||||
updated, err := initSystemTargets(cfg.HTTP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
swapMu.Lock()
|
||||
for _, tgt := range systemTargets {
|
||||
// Preserve console target when dynamically updating
|
||||
// other HTTP targets, console target is always present.
|
||||
if tgt.Type() == types.TargetConsole {
|
||||
updated = append(updated, tgt)
|
||||
break
|
||||
}
|
||||
}
|
||||
atomic.StoreInt32(&nTargets, int32(len(updated)))
|
||||
cancelAllSystemTargets() // cancel running targets
|
||||
systemTargets = updated
|
||||
swapMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func cancelAuditTargetType(t types.TargetType) {
|
||||
for _, tgt := range auditTargets {
|
||||
if tgt.Type() == t {
|
||||
tgt.Cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateAuditWebhookTargets swaps audit webhook targets with newly loaded ones from the cfg
|
||||
func UpdateAuditWebhookTargets(cfg Config) error {
|
||||
updated, err := initSystemTargets(cfg.AuditWebhook)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
swapMu.Lock()
|
||||
atomic.StoreInt32(&nAuditTargets, int32(len(updated)))
|
||||
cancelAuditTargetType(types.TargetHTTP) // cancel running targets
|
||||
auditTargets = updated
|
||||
swapMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
60
pkg/logger/utils.go
Normal file
60
pkg/logger/utils.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"runtime"
|
||||
|
||||
"github.com/minio/console/pkg/logger/color"
|
||||
)
|
||||
|
||||
var ansiRE = regexp.MustCompile("(\x1b[^m]*m)")
|
||||
|
||||
// Print ANSI Control escape
|
||||
func ansiEscape(format string, args ...interface{}) {
|
||||
Esc := "\x1b"
|
||||
fmt.Printf("%s%s", Esc, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func ansiMoveRight(n int) {
|
||||
if runtime.GOOS == "windows" {
|
||||
return
|
||||
}
|
||||
if color.IsTerminal() {
|
||||
ansiEscape("[%dC", n)
|
||||
}
|
||||
}
|
||||
|
||||
func ansiSaveAttributes() {
|
||||
if runtime.GOOS == "windows" {
|
||||
return
|
||||
}
|
||||
if color.IsTerminal() {
|
||||
ansiEscape("7")
|
||||
}
|
||||
}
|
||||
|
||||
func ansiRestoreAttributes() {
|
||||
if runtime.GOOS == "windows" {
|
||||
return
|
||||
}
|
||||
if color.IsTerminal() {
|
||||
ansiEscape("8")
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/minio/console/pkg/utils"
|
||||
"github.com/minio/console/pkg/http"
|
||||
|
||||
"github.com/minio/pkg/licverifier"
|
||||
|
||||
@@ -34,7 +34,7 @@ import (
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func LoginWithMFA(client utils.HTTPClientI, username, mfaToken, otp string) (*LoginResp, error) {
|
||||
func LoginWithMFA(client http.ClientI, username, mfaToken, otp string) (*LoginResp, error) {
|
||||
mfaLoginReq := MfaReq{Username: username, OTP: otp, Token: mfaToken}
|
||||
resp, err := subnetPostReq(client, subnetMFAURL(), mfaLoginReq, nil)
|
||||
if err != nil {
|
||||
@@ -47,7 +47,7 @@ func LoginWithMFA(client utils.HTTPClientI, username, mfaToken, otp string) (*Lo
|
||||
return nil, errors.New("access token not found in response")
|
||||
}
|
||||
|
||||
func Login(client utils.HTTPClientI, username, password string) (*LoginResp, error) {
|
||||
func Login(client http.ClientI, username, password string) (*LoginResp, error) {
|
||||
loginReq := map[string]string{
|
||||
"username": username,
|
||||
"password": password,
|
||||
@@ -71,7 +71,7 @@ func Login(client utils.HTTPClientI, username, password string) (*LoginResp, err
|
||||
return nil, errors.New("access token not found in response")
|
||||
}
|
||||
|
||||
func GetOrganizations(client utils.HTTPClientI, token string) ([]*models.SubnetOrganization, error) {
|
||||
func GetOrganizations(client http.ClientI, token string) ([]*models.SubnetOrganization, error) {
|
||||
headers := subnetAuthHeaders(token)
|
||||
respStr, err := subnetGetReq(client, subnetOrgsURL(), headers)
|
||||
if err != nil {
|
||||
@@ -90,7 +90,7 @@ type LicenseTokenConfig struct {
|
||||
Proxy string
|
||||
}
|
||||
|
||||
func Register(client utils.HTTPClientI, admInfo madmin.InfoMessage, apiKey, token, accountID string) (*LicenseTokenConfig, error) {
|
||||
func Register(client http.ClientI, admInfo madmin.InfoMessage, apiKey, token, accountID string) (*LicenseTokenConfig, error) {
|
||||
var headers map[string]string
|
||||
regInfo := GetClusterRegInfo(admInfo)
|
||||
regURL := subnetRegisterURL()
|
||||
@@ -128,7 +128,7 @@ func Register(client utils.HTTPClientI, admInfo madmin.InfoMessage, apiKey, toke
|
||||
const publicKey = "/downloads/license-pubkey.pem"
|
||||
|
||||
// downloadSubnetPublicKey will download the current subnet public key.
|
||||
func downloadSubnetPublicKey(client utils.HTTPClientI) (string, error) {
|
||||
func downloadSubnetPublicKey(client http.ClientI) (string, error) {
|
||||
// Get the public key directly from Subnet
|
||||
url := fmt.Sprintf("%s%s", subnetBaseURL(), publicKey)
|
||||
resp, err := client.Get(url)
|
||||
@@ -145,7 +145,7 @@ func downloadSubnetPublicKey(client utils.HTTPClientI) (string, error) {
|
||||
}
|
||||
|
||||
// ParseLicense parses the license with the bundle public key and return it's information
|
||||
func ParseLicense(client utils.HTTPClientI, license string) (*licverifier.LicenseInfo, error) {
|
||||
func ParseLicense(client http.ClientI, license string) (*licverifier.LicenseInfo, error) {
|
||||
var publicKeys []string
|
||||
|
||||
subnetPubKey, err := downloadSubnetPublicKey(client)
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/minio/console/pkg/utils"
|
||||
xhttp "github.com/minio/console/pkg/http"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
mc "github.com/minio/mc/cmd"
|
||||
@@ -69,11 +69,11 @@ func subnetAuthHeaders(authToken string) map[string]string {
|
||||
return map[string]string{"Authorization": "Bearer " + authToken}
|
||||
}
|
||||
|
||||
func httpDo(client utils.HTTPClientI, req *http.Request) (*http.Response, error) {
|
||||
func httpDo(client xhttp.ClientI, req *http.Request) (*http.Response, error) {
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
func subnetReqDo(client utils.HTTPClientI, r *http.Request, headers map[string]string) (string, error) {
|
||||
func subnetReqDo(client xhttp.ClientI, r *http.Request, headers map[string]string) (string, error) {
|
||||
for k, v := range headers {
|
||||
r.Header.Add(k, v)
|
||||
}
|
||||
@@ -98,10 +98,10 @@ func subnetReqDo(client utils.HTTPClientI, r *http.Request, headers map[string]s
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return respStr, nil
|
||||
}
|
||||
return respStr, fmt.Errorf("Request failed with code %d and error: %s", resp.StatusCode, respStr)
|
||||
return respStr, fmt.Errorf("Request failed with code %d and errors: %s", resp.StatusCode, respStr)
|
||||
}
|
||||
|
||||
func subnetGetReq(client utils.HTTPClientI, reqURL string, headers map[string]string) (string, error) {
|
||||
func subnetGetReq(client xhttp.ClientI, reqURL string, headers map[string]string) (string, error) {
|
||||
r, e := http.NewRequest(http.MethodGet, reqURL, nil)
|
||||
if e != nil {
|
||||
return "", e
|
||||
@@ -109,7 +109,7 @@ func subnetGetReq(client utils.HTTPClientI, reqURL string, headers map[string]st
|
||||
return subnetReqDo(client, r, headers)
|
||||
}
|
||||
|
||||
func subnetPostReq(client utils.HTTPClientI, reqURL string, payload interface{}, headers map[string]string) (string, error) {
|
||||
func subnetPostReq(client xhttp.ClientI, reqURL string, payload interface{}, headers map[string]string) (string, error) {
|
||||
body, e := json.Marshal(payload)
|
||||
if e != nil {
|
||||
return "", e
|
||||
|
||||
39
pkg/utils/utils.go
Normal file
39
pkg/utils/utils.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package utils
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
// NewUUID - get a random UUID.
|
||||
func NewUUID() (string, error) {
|
||||
u, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
// Key used for Get/SetReqInfo
|
||||
type key string
|
||||
|
||||
const ContextLogKey = key("console-log")
|
||||
const ContextRequestID = key("request-id")
|
||||
const ContextRequestUserID = key("request-user-id")
|
||||
const ContextRequestUserAgent = key("request-user-agent")
|
||||
const ContextRequestHost = key("request-host")
|
||||
const ContextRequestRemoteAddr = key("request-remote-addr")
|
||||
const ContextAuditKey = key("request-audit-entry")
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
|
||||
"github.com/minio/console/pkg/http"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -28,7 +30,7 @@ var (
|
||||
)
|
||||
|
||||
// getLatestMinIOImage returns the latest docker image for MinIO if found on the internet
|
||||
func GetLatestMinIOImage(client HTTPClientI) (*string, error) {
|
||||
func GetLatestMinIOImage(client http.ClientI) (*string, error) {
|
||||
resp, err := client.Get("https://dl.min.io/server/minio/release/linux-amd64/")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,121 +1,120 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.90d417ae.css",
|
||||
"main.js": "./static/js/main.eec275cb.js",
|
||||
"static/js/2483.48dd183a.chunk.js": "./static/js/2483.48dd183a.chunk.js",
|
||||
"main.js": "./static/js/main.7be4b5f3.js",
|
||||
"static/js/2483.ebdbad2e.chunk.js": "./static/js/2483.ebdbad2e.chunk.js",
|
||||
"static/js/6914.c9671304.chunk.js": "./static/js/6914.c9671304.chunk.js",
|
||||
"static/js/4209.b2656735.chunk.js": "./static/js/4209.b2656735.chunk.js",
|
||||
"static/js/1829.3a638c7d.chunk.js": "./static/js/1829.3a638c7d.chunk.js",
|
||||
"static/js/4209.741c5f3e.chunk.js": "./static/js/4209.741c5f3e.chunk.js",
|
||||
"static/js/1829.0ed9532d.chunk.js": "./static/js/1829.0ed9532d.chunk.js",
|
||||
"static/js/4455.4ba90afa.chunk.js": "./static/js/4455.4ba90afa.chunk.js",
|
||||
"static/js/5088.dae21c0f.chunk.js": "./static/js/5088.dae21c0f.chunk.js",
|
||||
"static/js/5088.07fbe469.chunk.js": "./static/js/5088.07fbe469.chunk.js",
|
||||
"static/js/5140.e9043b63.chunk.js": "./static/js/5140.e9043b63.chunk.js",
|
||||
"static/js/5646.e4b3b3e9.chunk.js": "./static/js/5646.e4b3b3e9.chunk.js",
|
||||
"static/js/5646.df015c08.chunk.js": "./static/js/5646.df015c08.chunk.js",
|
||||
"static/js/3176.43953acc.chunk.js": "./static/js/3176.43953acc.chunk.js",
|
||||
"static/js/6137.c0b24aaa.chunk.js": "./static/js/6137.c0b24aaa.chunk.js",
|
||||
"static/js/7045.ca5a5aae.chunk.js": "./static/js/7045.ca5a5aae.chunk.js",
|
||||
"static/js/9251.be8506b4.chunk.js": "./static/js/9251.be8506b4.chunk.js",
|
||||
"static/js/2338.8d87570c.chunk.js": "./static/js/2338.8d87570c.chunk.js",
|
||||
"static/js/4335.44360980.chunk.js": "./static/js/4335.44360980.chunk.js",
|
||||
"static/js/3061.c71568b9.chunk.js": "./static/js/3061.c71568b9.chunk.js",
|
||||
"static/js/6763.f0f3ae01.chunk.js": "./static/js/6763.f0f3ae01.chunk.js",
|
||||
"static/js/3543.2a18a6b9.chunk.js": "./static/js/3543.2a18a6b9.chunk.js",
|
||||
"static/js/4061.44b5ee07.chunk.js": "./static/js/4061.44b5ee07.chunk.js",
|
||||
"static/js/2249.eff0718f.chunk.js": "./static/js/2249.eff0718f.chunk.js",
|
||||
"static/js/9251.39470f5c.chunk.js": "./static/js/9251.39470f5c.chunk.js",
|
||||
"static/js/2338.b996d7f6.chunk.js": "./static/js/2338.b996d7f6.chunk.js",
|
||||
"static/js/4335.48b6eb71.chunk.js": "./static/js/4335.48b6eb71.chunk.js",
|
||||
"static/js/3061.fdf907c5.chunk.js": "./static/js/3061.fdf907c5.chunk.js",
|
||||
"static/js/6763.8e2c073b.chunk.js": "./static/js/6763.8e2c073b.chunk.js",
|
||||
"static/js/3543.7da7dc0a.chunk.js": "./static/js/3543.7da7dc0a.chunk.js",
|
||||
"static/js/4061.9a8a13eb.chunk.js": "./static/js/4061.9a8a13eb.chunk.js",
|
||||
"static/js/2249.6f8e7e92.chunk.js": "./static/js/2249.6f8e7e92.chunk.js",
|
||||
"static/js/9611.c217768e.chunk.js": "./static/js/9611.c217768e.chunk.js",
|
||||
"static/js/2637.6d2abbec.chunk.js": "./static/js/2637.6d2abbec.chunk.js",
|
||||
"static/css/380.8313f811.chunk.css": "./static/css/380.8313f811.chunk.css",
|
||||
"static/js/380.78d0d6a2.chunk.js": "./static/js/380.78d0d6a2.chunk.js",
|
||||
"static/js/5926.4b729c3b.chunk.js": "./static/js/5926.4b729c3b.chunk.js",
|
||||
"static/js/2637.02eefe4a.chunk.js": "./static/js/2637.02eefe4a.chunk.js",
|
||||
"static/css/380.04346438.chunk.css": "./static/css/380.04346438.chunk.css",
|
||||
"static/js/380.8fcb1392.chunk.js": "./static/js/380.8fcb1392.chunk.js",
|
||||
"static/js/5926.31770a21.chunk.js": "./static/js/5926.31770a21.chunk.js",
|
||||
"static/js/701.52180c55.chunk.js": "./static/js/701.52180c55.chunk.js",
|
||||
"static/js/7821.a2c85c06.chunk.js": "./static/js/7821.a2c85c06.chunk.js",
|
||||
"static/css/2080.8313f811.chunk.css": "./static/css/2080.8313f811.chunk.css",
|
||||
"static/js/2080.5b870317.chunk.js": "./static/js/2080.5b870317.chunk.js",
|
||||
"static/js/6747.a78bbd22.chunk.js": "./static/js/6747.a78bbd22.chunk.js",
|
||||
"static/css/9033.8313f811.chunk.css": "./static/css/9033.8313f811.chunk.css",
|
||||
"static/js/9033.b95c6d4a.chunk.js": "./static/js/9033.b95c6d4a.chunk.js",
|
||||
"static/css/3368.8313f811.chunk.css": "./static/css/3368.8313f811.chunk.css",
|
||||
"static/js/3368.27a61e61.chunk.js": "./static/js/3368.27a61e61.chunk.js",
|
||||
"static/css/3688.8313f811.chunk.css": "./static/css/3688.8313f811.chunk.css",
|
||||
"static/js/3688.fa756e3b.chunk.js": "./static/js/3688.fa756e3b.chunk.js",
|
||||
"static/js/2555.cd5bfa20.chunk.js": "./static/js/2555.cd5bfa20.chunk.js",
|
||||
"static/js/7585.78d525ce.chunk.js": "./static/js/7585.78d525ce.chunk.js",
|
||||
"static/js/1836.86b53328.chunk.js": "./static/js/1836.86b53328.chunk.js",
|
||||
"static/js/4653.89f5d861.chunk.js": "./static/js/4653.89f5d861.chunk.js",
|
||||
"static/js/4219.c24a76ef.chunk.js": "./static/js/4219.c24a76ef.chunk.js",
|
||||
"static/js/8626.896fa8ac.chunk.js": "./static/js/8626.896fa8ac.chunk.js",
|
||||
"static/js/736.40b3a390.chunk.js": "./static/js/736.40b3a390.chunk.js",
|
||||
"static/js/6577.d48f6fc0.chunk.js": "./static/js/6577.d48f6fc0.chunk.js",
|
||||
"static/js/9561.46bb02ad.chunk.js": "./static/js/9561.46bb02ad.chunk.js",
|
||||
"static/js/4394.05e77f06.chunk.js": "./static/js/4394.05e77f06.chunk.js",
|
||||
"static/css/2080.04346438.chunk.css": "./static/css/2080.04346438.chunk.css",
|
||||
"static/js/2080.07b74e77.chunk.js": "./static/js/2080.07b74e77.chunk.js",
|
||||
"static/js/4352.ecb9b923.chunk.js": "./static/js/4352.ecb9b923.chunk.js",
|
||||
"static/css/9033.04346438.chunk.css": "./static/css/9033.04346438.chunk.css",
|
||||
"static/js/9033.e6865183.chunk.js": "./static/js/9033.e6865183.chunk.js",
|
||||
"static/css/6633.04346438.chunk.css": "./static/css/6633.04346438.chunk.css",
|
||||
"static/js/6633.2d6d234a.chunk.js": "./static/js/6633.2d6d234a.chunk.js",
|
||||
"static/css/6859.04346438.chunk.css": "./static/css/6859.04346438.chunk.css",
|
||||
"static/js/6859.e8e4a97d.chunk.js": "./static/js/6859.e8e4a97d.chunk.js",
|
||||
"static/js/2555.f9ba93a3.chunk.js": "./static/js/2555.f9ba93a3.chunk.js",
|
||||
"static/js/7585.a08ac645.chunk.js": "./static/js/7585.a08ac645.chunk.js",
|
||||
"static/js/1836.b42dfba9.chunk.js": "./static/js/1836.b42dfba9.chunk.js",
|
||||
"static/js/4653.f372ea27.chunk.js": "./static/js/4653.f372ea27.chunk.js",
|
||||
"static/js/4219.e5abf39a.chunk.js": "./static/js/4219.e5abf39a.chunk.js",
|
||||
"static/js/8626.cae557ae.chunk.js": "./static/js/8626.cae557ae.chunk.js",
|
||||
"static/js/736.853a950a.chunk.js": "./static/js/736.853a950a.chunk.js",
|
||||
"static/js/6577.c8b36701.chunk.js": "./static/js/6577.c8b36701.chunk.js",
|
||||
"static/js/9561.dbb650ca.chunk.js": "./static/js/9561.dbb650ca.chunk.js",
|
||||
"static/js/4394.132a2087.chunk.js": "./static/js/4394.132a2087.chunk.js",
|
||||
"static/js/4781.785d14ba.chunk.js": "./static/js/4781.785d14ba.chunk.js",
|
||||
"static/js/9478.7c40d91e.chunk.js": "./static/js/9478.7c40d91e.chunk.js",
|
||||
"static/js/7164.5542a849.chunk.js": "./static/js/7164.5542a849.chunk.js",
|
||||
"static/js/4414.d9f98702.chunk.js": "./static/js/4414.d9f98702.chunk.js",
|
||||
"static/js/7798.07093714.chunk.js": "./static/js/7798.07093714.chunk.js",
|
||||
"static/js/8833.60296031.chunk.js": "./static/js/8833.60296031.chunk.js",
|
||||
"static/js/471.244e997d.chunk.js": "./static/js/471.244e997d.chunk.js",
|
||||
"static/js/483.a8ef0ad5.chunk.js": "./static/js/483.a8ef0ad5.chunk.js",
|
||||
"static/js/9467.5fb2ba42.chunk.js": "./static/js/9467.5fb2ba42.chunk.js",
|
||||
"static/js/6895.10b5757c.chunk.js": "./static/js/6895.10b5757c.chunk.js",
|
||||
"static/js/6233.c0a85e71.chunk.js": "./static/js/6233.c0a85e71.chunk.js",
|
||||
"static/js/5588.fa4e23ec.chunk.js": "./static/js/5588.fa4e23ec.chunk.js",
|
||||
"static/js/4133.1b71a3ec.chunk.js": "./static/js/4133.1b71a3ec.chunk.js",
|
||||
"static/css/1955.8313f811.chunk.css": "./static/css/1955.8313f811.chunk.css",
|
||||
"static/js/1955.fcaedc3e.chunk.js": "./static/js/1955.fcaedc3e.chunk.js",
|
||||
"static/js/3956.0b36ab08.chunk.js": "./static/js/3956.0b36ab08.chunk.js",
|
||||
"static/js/4414.95bb9b72.chunk.js": "./static/js/4414.95bb9b72.chunk.js",
|
||||
"static/js/7798.36139559.chunk.js": "./static/js/7798.36139559.chunk.js",
|
||||
"static/js/8833.8c3d2d1a.chunk.js": "./static/js/8833.8c3d2d1a.chunk.js",
|
||||
"static/js/471.cc100d23.chunk.js": "./static/js/471.cc100d23.chunk.js",
|
||||
"static/js/483.14f274ee.chunk.js": "./static/js/483.14f274ee.chunk.js",
|
||||
"static/js/9467.22552611.chunk.js": "./static/js/9467.22552611.chunk.js",
|
||||
"static/js/6895.403b3023.chunk.js": "./static/js/6895.403b3023.chunk.js",
|
||||
"static/js/6233.dc9e7015.chunk.js": "./static/js/6233.dc9e7015.chunk.js",
|
||||
"static/js/5588.358d372f.chunk.js": "./static/js/5588.358d372f.chunk.js",
|
||||
"static/js/4133.d301b843.chunk.js": "./static/js/4133.d301b843.chunk.js",
|
||||
"static/css/1955.04346438.chunk.css": "./static/css/1955.04346438.chunk.css",
|
||||
"static/js/1955.d60ad4bd.chunk.js": "./static/js/1955.d60ad4bd.chunk.js",
|
||||
"static/js/3956.bc3ae9b9.chunk.js": "./static/js/3956.bc3ae9b9.chunk.js",
|
||||
"static/js/8771.475684ba.chunk.js": "./static/js/8771.475684ba.chunk.js",
|
||||
"static/js/9076.a6606f2e.chunk.js": "./static/js/9076.a6606f2e.chunk.js",
|
||||
"static/js/9221.ba80075c.chunk.js": "./static/js/9221.ba80075c.chunk.js",
|
||||
"static/js/8896.09fd2a0c.chunk.js": "./static/js/8896.09fd2a0c.chunk.js",
|
||||
"static/js/7413.73b72a2b.chunk.js": "./static/js/7413.73b72a2b.chunk.js",
|
||||
"static/js/9076.596078c8.chunk.js": "./static/js/9076.596078c8.chunk.js",
|
||||
"static/js/9221.afc1d068.chunk.js": "./static/js/9221.afc1d068.chunk.js",
|
||||
"static/js/8896.ea205ba2.chunk.js": "./static/js/8896.ea205ba2.chunk.js",
|
||||
"static/js/9134.b0935ef3.chunk.js": "./static/js/9134.b0935ef3.chunk.js",
|
||||
"static/css/8138.8313f811.chunk.css": "./static/css/8138.8313f811.chunk.css",
|
||||
"static/js/8138.e9fa48a9.chunk.js": "./static/js/8138.e9fa48a9.chunk.js",
|
||||
"static/js/1030.4a654568.chunk.js": "./static/js/1030.4a654568.chunk.js",
|
||||
"static/css/8138.04346438.chunk.css": "./static/css/8138.04346438.chunk.css",
|
||||
"static/js/8138.f97957dc.chunk.js": "./static/js/8138.f97957dc.chunk.js",
|
||||
"static/js/1030.986c2667.chunk.js": "./static/js/1030.986c2667.chunk.js",
|
||||
"static/js/9145.363b2352.chunk.js": "./static/js/9145.363b2352.chunk.js",
|
||||
"static/js/1379.0bfc0b60.chunk.js": "./static/js/1379.0bfc0b60.chunk.js",
|
||||
"static/js/1501.c46671fd.chunk.js": "./static/js/1501.c46671fd.chunk.js",
|
||||
"static/js/9605.c3d4a4cf.chunk.js": "./static/js/9605.c3d4a4cf.chunk.js",
|
||||
"static/js/426.e738683c.chunk.js": "./static/js/426.e738683c.chunk.js",
|
||||
"static/js/1379.7cd1ce08.chunk.js": "./static/js/1379.7cd1ce08.chunk.js",
|
||||
"static/js/1501.44151d90.chunk.js": "./static/js/1501.44151d90.chunk.js",
|
||||
"static/js/9605.1542e970.chunk.js": "./static/js/9605.1542e970.chunk.js",
|
||||
"static/js/426.1a73abd5.chunk.js": "./static/js/426.1a73abd5.chunk.js",
|
||||
"static/js/2878.fca6e2cf.chunk.js": "./static/js/2878.fca6e2cf.chunk.js",
|
||||
"static/js/8495.bdd215dc.chunk.js": "./static/js/8495.bdd215dc.chunk.js",
|
||||
"static/js/4934.4a573b0b.chunk.js": "./static/js/4934.4a573b0b.chunk.js",
|
||||
"static/js/3518.e1923a22.chunk.js": "./static/js/3518.e1923a22.chunk.js",
|
||||
"static/js/7021.ea551e6b.chunk.js": "./static/js/7021.ea551e6b.chunk.js",
|
||||
"static/js/2684.569c8172.chunk.js": "./static/js/2684.569c8172.chunk.js",
|
||||
"static/js/6683.53f69e13.chunk.js": "./static/js/6683.53f69e13.chunk.js",
|
||||
"static/js/8350.031612d5.chunk.js": "./static/js/8350.031612d5.chunk.js",
|
||||
"static/js/7021.8869219f.chunk.js": "./static/js/7021.8869219f.chunk.js",
|
||||
"static/js/2684.8bdcdf4b.chunk.js": "./static/js/2684.8bdcdf4b.chunk.js",
|
||||
"static/js/6683.dc4f1821.chunk.js": "./static/js/6683.dc4f1821.chunk.js",
|
||||
"static/js/8350.8e2fd461.chunk.js": "./static/js/8350.8e2fd461.chunk.js",
|
||||
"static/js/2676.bd3d9df3.chunk.js": "./static/js/2676.bd3d9df3.chunk.js",
|
||||
"static/js/9449.83d73a19.chunk.js": "./static/js/9449.83d73a19.chunk.js",
|
||||
"static/js/7659.5154337d.chunk.js": "./static/js/7659.5154337d.chunk.js",
|
||||
"static/js/9968.a7a1674d.chunk.js": "./static/js/9968.a7a1674d.chunk.js",
|
||||
"static/js/2180.c83301fc.chunk.js": "./static/js/2180.c83301fc.chunk.js",
|
||||
"static/js/9449.d160d883.chunk.js": "./static/js/9449.d160d883.chunk.js",
|
||||
"static/js/7659.845e0cad.chunk.js": "./static/js/7659.845e0cad.chunk.js",
|
||||
"static/js/9968.e41064fb.chunk.js": "./static/js/9968.e41064fb.chunk.js",
|
||||
"static/js/2180.ec9a5c77.chunk.js": "./static/js/2180.ec9a5c77.chunk.js",
|
||||
"static/js/8253.964026c0.chunk.js": "./static/js/8253.964026c0.chunk.js",
|
||||
"static/js/3328.da1cf1c8.chunk.js": "./static/js/3328.da1cf1c8.chunk.js",
|
||||
"static/js/3328.c97cfa34.chunk.js": "./static/js/3328.c97cfa34.chunk.js",
|
||||
"static/js/1440.74dce637.chunk.js": "./static/js/1440.74dce637.chunk.js",
|
||||
"static/js/2512.acfc57ce.chunk.js": "./static/js/2512.acfc57ce.chunk.js",
|
||||
"static/js/51.63259724.chunk.js": "./static/js/51.63259724.chunk.js",
|
||||
"static/js/711.5cec9776.chunk.js": "./static/js/711.5cec9776.chunk.js",
|
||||
"static/js/6901.53c6aef7.chunk.js": "./static/js/6901.53c6aef7.chunk.js",
|
||||
"static/js/2185.95c76a1b.chunk.js": "./static/js/2185.95c76a1b.chunk.js",
|
||||
"static/js/312.d183c8e0.chunk.js": "./static/js/312.d183c8e0.chunk.js",
|
||||
"static/js/2112.48f0caa6.chunk.js": "./static/js/2112.48f0caa6.chunk.js",
|
||||
"static/js/4619.f7970b8a.chunk.js": "./static/js/4619.f7970b8a.chunk.js",
|
||||
"static/js/8990.18a36cac.chunk.js": "./static/js/8990.18a36cac.chunk.js",
|
||||
"static/js/8455.0cd71acb.chunk.js": "./static/js/8455.0cd71acb.chunk.js",
|
||||
"static/css/3631.8313f811.chunk.css": "./static/css/3631.8313f811.chunk.css",
|
||||
"static/js/3631.64015ba0.chunk.js": "./static/js/3631.64015ba0.chunk.js",
|
||||
"static/js/2512.a00182cb.chunk.js": "./static/js/2512.a00182cb.chunk.js",
|
||||
"static/js/51.7a2ef89a.chunk.js": "./static/js/51.7a2ef89a.chunk.js",
|
||||
"static/js/711.71917357.chunk.js": "./static/js/711.71917357.chunk.js",
|
||||
"static/js/6901.9f0fcbd5.chunk.js": "./static/js/6901.9f0fcbd5.chunk.js",
|
||||
"static/js/2185.50d8f062.chunk.js": "./static/js/2185.50d8f062.chunk.js",
|
||||
"static/js/312.a4c03743.chunk.js": "./static/js/312.a4c03743.chunk.js",
|
||||
"static/js/2112.4691ccbf.chunk.js": "./static/js/2112.4691ccbf.chunk.js",
|
||||
"static/js/4619.6dd3ad70.chunk.js": "./static/js/4619.6dd3ad70.chunk.js",
|
||||
"static/js/8990.181bff03.chunk.js": "./static/js/8990.181bff03.chunk.js",
|
||||
"static/js/8455.27bfde53.chunk.js": "./static/js/8455.27bfde53.chunk.js",
|
||||
"static/css/3631.04346438.chunk.css": "./static/css/3631.04346438.chunk.css",
|
||||
"static/js/3631.5db9f8b6.chunk.js": "./static/js/3631.5db9f8b6.chunk.js",
|
||||
"static/js/1604.a9d0b62b.chunk.js": "./static/js/1604.a9d0b62b.chunk.js",
|
||||
"static/js/8391.7c39b52d.chunk.js": "./static/js/8391.7c39b52d.chunk.js",
|
||||
"static/js/402.0fe11251.chunk.js": "./static/js/402.0fe11251.chunk.js",
|
||||
"static/js/8391.a94a5da9.chunk.js": "./static/js/8391.a94a5da9.chunk.js",
|
||||
"static/js/402.087a85de.chunk.js": "./static/js/402.087a85de.chunk.js",
|
||||
"static/js/1705.5e57fd31.chunk.js": "./static/js/1705.5e57fd31.chunk.js",
|
||||
"static/js/1581.e5ea40c0.chunk.js": "./static/js/1581.e5ea40c0.chunk.js",
|
||||
"static/js/455.0218ce38.chunk.js": "./static/js/455.0218ce38.chunk.js",
|
||||
"static/js/2661.2121f536.chunk.js": "./static/js/2661.2121f536.chunk.js",
|
||||
"static/js/889.3d385602.chunk.js": "./static/js/889.3d385602.chunk.js",
|
||||
"static/js/9088.e252c094.chunk.js": "./static/js/9088.e252c094.chunk.js",
|
||||
"static/js/247.88de16aa.chunk.js": "./static/js/247.88de16aa.chunk.js",
|
||||
"static/js/2763.44403c2a.chunk.js": "./static/js/2763.44403c2a.chunk.js",
|
||||
"static/js/1581.cbc3b85a.chunk.js": "./static/js/1581.cbc3b85a.chunk.js",
|
||||
"static/js/455.dd2c1a2c.chunk.js": "./static/js/455.dd2c1a2c.chunk.js",
|
||||
"static/js/2661.ffb402c6.chunk.js": "./static/js/2661.ffb402c6.chunk.js",
|
||||
"static/js/889.34a82de2.chunk.js": "./static/js/889.34a82de2.chunk.js",
|
||||
"static/js/9088.edd377c6.chunk.js": "./static/js/9088.edd377c6.chunk.js",
|
||||
"static/js/247.10687506.chunk.js": "./static/js/247.10687506.chunk.js",
|
||||
"static/js/2763.247d377c.chunk.js": "./static/js/2763.247d377c.chunk.js",
|
||||
"static/js/5171.2cf876b1.chunk.js": "./static/js/5171.2cf876b1.chunk.js",
|
||||
"static/js/2426.172b5361.chunk.js": "./static/js/2426.172b5361.chunk.js",
|
||||
"static/js/5561.c5000912.chunk.js": "./static/js/5561.c5000912.chunk.js",
|
||||
@@ -144,126 +143,125 @@
|
||||
"static/js/9785.7ccf0212.chunk.js": "./static/js/9785.7ccf0212.chunk.js",
|
||||
"static/js/8735.52726eac.chunk.js": "./static/js/8735.52726eac.chunk.js",
|
||||
"static/js/63.830fd6fc.chunk.js": "./static/js/63.830fd6fc.chunk.js",
|
||||
"static/js/2983.66cf0ad4.chunk.js": "./static/js/2983.66cf0ad4.chunk.js",
|
||||
"static/js/5289.8c388542.chunk.js": "./static/js/5289.8c388542.chunk.js",
|
||||
"static/js/5026.0b30f6e2.chunk.js": "./static/js/5026.0b30f6e2.chunk.js",
|
||||
"static/js/2983.3e4b1a45.chunk.js": "./static/js/2983.3e4b1a45.chunk.js",
|
||||
"static/js/5289.1434b8bc.chunk.js": "./static/js/5289.1434b8bc.chunk.js",
|
||||
"static/js/5026.ebd99276.chunk.js": "./static/js/5026.ebd99276.chunk.js",
|
||||
"index.html": "./index.html",
|
||||
"main.90d417ae.css.map": "./static/css/main.90d417ae.css.map",
|
||||
"main.eec275cb.js.map": "./static/js/main.eec275cb.js.map",
|
||||
"2483.48dd183a.chunk.js.map": "./static/js/2483.48dd183a.chunk.js.map",
|
||||
"main.7be4b5f3.js.map": "./static/js/main.7be4b5f3.js.map",
|
||||
"2483.ebdbad2e.chunk.js.map": "./static/js/2483.ebdbad2e.chunk.js.map",
|
||||
"6914.c9671304.chunk.js.map": "./static/js/6914.c9671304.chunk.js.map",
|
||||
"4209.b2656735.chunk.js.map": "./static/js/4209.b2656735.chunk.js.map",
|
||||
"1829.3a638c7d.chunk.js.map": "./static/js/1829.3a638c7d.chunk.js.map",
|
||||
"4209.741c5f3e.chunk.js.map": "./static/js/4209.741c5f3e.chunk.js.map",
|
||||
"1829.0ed9532d.chunk.js.map": "./static/js/1829.0ed9532d.chunk.js.map",
|
||||
"4455.4ba90afa.chunk.js.map": "./static/js/4455.4ba90afa.chunk.js.map",
|
||||
"5088.dae21c0f.chunk.js.map": "./static/js/5088.dae21c0f.chunk.js.map",
|
||||
"5088.07fbe469.chunk.js.map": "./static/js/5088.07fbe469.chunk.js.map",
|
||||
"5140.e9043b63.chunk.js.map": "./static/js/5140.e9043b63.chunk.js.map",
|
||||
"5646.e4b3b3e9.chunk.js.map": "./static/js/5646.e4b3b3e9.chunk.js.map",
|
||||
"5646.df015c08.chunk.js.map": "./static/js/5646.df015c08.chunk.js.map",
|
||||
"3176.43953acc.chunk.js.map": "./static/js/3176.43953acc.chunk.js.map",
|
||||
"6137.c0b24aaa.chunk.js.map": "./static/js/6137.c0b24aaa.chunk.js.map",
|
||||
"7045.ca5a5aae.chunk.js.map": "./static/js/7045.ca5a5aae.chunk.js.map",
|
||||
"9251.be8506b4.chunk.js.map": "./static/js/9251.be8506b4.chunk.js.map",
|
||||
"2338.8d87570c.chunk.js.map": "./static/js/2338.8d87570c.chunk.js.map",
|
||||
"4335.44360980.chunk.js.map": "./static/js/4335.44360980.chunk.js.map",
|
||||
"3061.c71568b9.chunk.js.map": "./static/js/3061.c71568b9.chunk.js.map",
|
||||
"6763.f0f3ae01.chunk.js.map": "./static/js/6763.f0f3ae01.chunk.js.map",
|
||||
"3543.2a18a6b9.chunk.js.map": "./static/js/3543.2a18a6b9.chunk.js.map",
|
||||
"4061.44b5ee07.chunk.js.map": "./static/js/4061.44b5ee07.chunk.js.map",
|
||||
"2249.eff0718f.chunk.js.map": "./static/js/2249.eff0718f.chunk.js.map",
|
||||
"9251.39470f5c.chunk.js.map": "./static/js/9251.39470f5c.chunk.js.map",
|
||||
"2338.b996d7f6.chunk.js.map": "./static/js/2338.b996d7f6.chunk.js.map",
|
||||
"4335.48b6eb71.chunk.js.map": "./static/js/4335.48b6eb71.chunk.js.map",
|
||||
"3061.fdf907c5.chunk.js.map": "./static/js/3061.fdf907c5.chunk.js.map",
|
||||
"6763.8e2c073b.chunk.js.map": "./static/js/6763.8e2c073b.chunk.js.map",
|
||||
"3543.7da7dc0a.chunk.js.map": "./static/js/3543.7da7dc0a.chunk.js.map",
|
||||
"4061.9a8a13eb.chunk.js.map": "./static/js/4061.9a8a13eb.chunk.js.map",
|
||||
"2249.6f8e7e92.chunk.js.map": "./static/js/2249.6f8e7e92.chunk.js.map",
|
||||
"9611.c217768e.chunk.js.map": "./static/js/9611.c217768e.chunk.js.map",
|
||||
"2637.6d2abbec.chunk.js.map": "./static/js/2637.6d2abbec.chunk.js.map",
|
||||
"380.8313f811.chunk.css.map": "./static/css/380.8313f811.chunk.css.map",
|
||||
"380.78d0d6a2.chunk.js.map": "./static/js/380.78d0d6a2.chunk.js.map",
|
||||
"5926.4b729c3b.chunk.js.map": "./static/js/5926.4b729c3b.chunk.js.map",
|
||||
"2637.02eefe4a.chunk.js.map": "./static/js/2637.02eefe4a.chunk.js.map",
|
||||
"380.04346438.chunk.css.map": "./static/css/380.04346438.chunk.css.map",
|
||||
"380.8fcb1392.chunk.js.map": "./static/js/380.8fcb1392.chunk.js.map",
|
||||
"5926.31770a21.chunk.js.map": "./static/js/5926.31770a21.chunk.js.map",
|
||||
"701.52180c55.chunk.js.map": "./static/js/701.52180c55.chunk.js.map",
|
||||
"7821.a2c85c06.chunk.js.map": "./static/js/7821.a2c85c06.chunk.js.map",
|
||||
"2080.8313f811.chunk.css.map": "./static/css/2080.8313f811.chunk.css.map",
|
||||
"2080.5b870317.chunk.js.map": "./static/js/2080.5b870317.chunk.js.map",
|
||||
"6747.a78bbd22.chunk.js.map": "./static/js/6747.a78bbd22.chunk.js.map",
|
||||
"9033.8313f811.chunk.css.map": "./static/css/9033.8313f811.chunk.css.map",
|
||||
"9033.b95c6d4a.chunk.js.map": "./static/js/9033.b95c6d4a.chunk.js.map",
|
||||
"3368.8313f811.chunk.css.map": "./static/css/3368.8313f811.chunk.css.map",
|
||||
"3368.27a61e61.chunk.js.map": "./static/js/3368.27a61e61.chunk.js.map",
|
||||
"3688.8313f811.chunk.css.map": "./static/css/3688.8313f811.chunk.css.map",
|
||||
"3688.fa756e3b.chunk.js.map": "./static/js/3688.fa756e3b.chunk.js.map",
|
||||
"2555.cd5bfa20.chunk.js.map": "./static/js/2555.cd5bfa20.chunk.js.map",
|
||||
"7585.78d525ce.chunk.js.map": "./static/js/7585.78d525ce.chunk.js.map",
|
||||
"1836.86b53328.chunk.js.map": "./static/js/1836.86b53328.chunk.js.map",
|
||||
"4653.89f5d861.chunk.js.map": "./static/js/4653.89f5d861.chunk.js.map",
|
||||
"4219.c24a76ef.chunk.js.map": "./static/js/4219.c24a76ef.chunk.js.map",
|
||||
"8626.896fa8ac.chunk.js.map": "./static/js/8626.896fa8ac.chunk.js.map",
|
||||
"736.40b3a390.chunk.js.map": "./static/js/736.40b3a390.chunk.js.map",
|
||||
"6577.d48f6fc0.chunk.js.map": "./static/js/6577.d48f6fc0.chunk.js.map",
|
||||
"9561.46bb02ad.chunk.js.map": "./static/js/9561.46bb02ad.chunk.js.map",
|
||||
"4394.05e77f06.chunk.js.map": "./static/js/4394.05e77f06.chunk.js.map",
|
||||
"2080.04346438.chunk.css.map": "./static/css/2080.04346438.chunk.css.map",
|
||||
"2080.07b74e77.chunk.js.map": "./static/js/2080.07b74e77.chunk.js.map",
|
||||
"4352.ecb9b923.chunk.js.map": "./static/js/4352.ecb9b923.chunk.js.map",
|
||||
"9033.04346438.chunk.css.map": "./static/css/9033.04346438.chunk.css.map",
|
||||
"9033.e6865183.chunk.js.map": "./static/js/9033.e6865183.chunk.js.map",
|
||||
"6633.04346438.chunk.css.map": "./static/css/6633.04346438.chunk.css.map",
|
||||
"6633.2d6d234a.chunk.js.map": "./static/js/6633.2d6d234a.chunk.js.map",
|
||||
"6859.04346438.chunk.css.map": "./static/css/6859.04346438.chunk.css.map",
|
||||
"6859.e8e4a97d.chunk.js.map": "./static/js/6859.e8e4a97d.chunk.js.map",
|
||||
"2555.f9ba93a3.chunk.js.map": "./static/js/2555.f9ba93a3.chunk.js.map",
|
||||
"7585.a08ac645.chunk.js.map": "./static/js/7585.a08ac645.chunk.js.map",
|
||||
"1836.b42dfba9.chunk.js.map": "./static/js/1836.b42dfba9.chunk.js.map",
|
||||
"4653.f372ea27.chunk.js.map": "./static/js/4653.f372ea27.chunk.js.map",
|
||||
"4219.e5abf39a.chunk.js.map": "./static/js/4219.e5abf39a.chunk.js.map",
|
||||
"8626.cae557ae.chunk.js.map": "./static/js/8626.cae557ae.chunk.js.map",
|
||||
"736.853a950a.chunk.js.map": "./static/js/736.853a950a.chunk.js.map",
|
||||
"6577.c8b36701.chunk.js.map": "./static/js/6577.c8b36701.chunk.js.map",
|
||||
"9561.dbb650ca.chunk.js.map": "./static/js/9561.dbb650ca.chunk.js.map",
|
||||
"4394.132a2087.chunk.js.map": "./static/js/4394.132a2087.chunk.js.map",
|
||||
"4781.785d14ba.chunk.js.map": "./static/js/4781.785d14ba.chunk.js.map",
|
||||
"9478.7c40d91e.chunk.js.map": "./static/js/9478.7c40d91e.chunk.js.map",
|
||||
"7164.5542a849.chunk.js.map": "./static/js/7164.5542a849.chunk.js.map",
|
||||
"4414.d9f98702.chunk.js.map": "./static/js/4414.d9f98702.chunk.js.map",
|
||||
"7798.07093714.chunk.js.map": "./static/js/7798.07093714.chunk.js.map",
|
||||
"8833.60296031.chunk.js.map": "./static/js/8833.60296031.chunk.js.map",
|
||||
"471.244e997d.chunk.js.map": "./static/js/471.244e997d.chunk.js.map",
|
||||
"483.a8ef0ad5.chunk.js.map": "./static/js/483.a8ef0ad5.chunk.js.map",
|
||||
"9467.5fb2ba42.chunk.js.map": "./static/js/9467.5fb2ba42.chunk.js.map",
|
||||
"6895.10b5757c.chunk.js.map": "./static/js/6895.10b5757c.chunk.js.map",
|
||||
"6233.c0a85e71.chunk.js.map": "./static/js/6233.c0a85e71.chunk.js.map",
|
||||
"5588.fa4e23ec.chunk.js.map": "./static/js/5588.fa4e23ec.chunk.js.map",
|
||||
"4133.1b71a3ec.chunk.js.map": "./static/js/4133.1b71a3ec.chunk.js.map",
|
||||
"1955.8313f811.chunk.css.map": "./static/css/1955.8313f811.chunk.css.map",
|
||||
"1955.fcaedc3e.chunk.js.map": "./static/js/1955.fcaedc3e.chunk.js.map",
|
||||
"3956.0b36ab08.chunk.js.map": "./static/js/3956.0b36ab08.chunk.js.map",
|
||||
"4414.95bb9b72.chunk.js.map": "./static/js/4414.95bb9b72.chunk.js.map",
|
||||
"7798.36139559.chunk.js.map": "./static/js/7798.36139559.chunk.js.map",
|
||||
"8833.8c3d2d1a.chunk.js.map": "./static/js/8833.8c3d2d1a.chunk.js.map",
|
||||
"471.cc100d23.chunk.js.map": "./static/js/471.cc100d23.chunk.js.map",
|
||||
"483.14f274ee.chunk.js.map": "./static/js/483.14f274ee.chunk.js.map",
|
||||
"9467.22552611.chunk.js.map": "./static/js/9467.22552611.chunk.js.map",
|
||||
"6895.403b3023.chunk.js.map": "./static/js/6895.403b3023.chunk.js.map",
|
||||
"6233.dc9e7015.chunk.js.map": "./static/js/6233.dc9e7015.chunk.js.map",
|
||||
"5588.358d372f.chunk.js.map": "./static/js/5588.358d372f.chunk.js.map",
|
||||
"4133.d301b843.chunk.js.map": "./static/js/4133.d301b843.chunk.js.map",
|
||||
"1955.04346438.chunk.css.map": "./static/css/1955.04346438.chunk.css.map",
|
||||
"1955.d60ad4bd.chunk.js.map": "./static/js/1955.d60ad4bd.chunk.js.map",
|
||||
"3956.bc3ae9b9.chunk.js.map": "./static/js/3956.bc3ae9b9.chunk.js.map",
|
||||
"8771.475684ba.chunk.js.map": "./static/js/8771.475684ba.chunk.js.map",
|
||||
"9076.a6606f2e.chunk.js.map": "./static/js/9076.a6606f2e.chunk.js.map",
|
||||
"9221.ba80075c.chunk.js.map": "./static/js/9221.ba80075c.chunk.js.map",
|
||||
"8896.09fd2a0c.chunk.js.map": "./static/js/8896.09fd2a0c.chunk.js.map",
|
||||
"7413.73b72a2b.chunk.js.map": "./static/js/7413.73b72a2b.chunk.js.map",
|
||||
"9076.596078c8.chunk.js.map": "./static/js/9076.596078c8.chunk.js.map",
|
||||
"9221.afc1d068.chunk.js.map": "./static/js/9221.afc1d068.chunk.js.map",
|
||||
"8896.ea205ba2.chunk.js.map": "./static/js/8896.ea205ba2.chunk.js.map",
|
||||
"9134.b0935ef3.chunk.js.map": "./static/js/9134.b0935ef3.chunk.js.map",
|
||||
"8138.8313f811.chunk.css.map": "./static/css/8138.8313f811.chunk.css.map",
|
||||
"8138.e9fa48a9.chunk.js.map": "./static/js/8138.e9fa48a9.chunk.js.map",
|
||||
"1030.4a654568.chunk.js.map": "./static/js/1030.4a654568.chunk.js.map",
|
||||
"8138.04346438.chunk.css.map": "./static/css/8138.04346438.chunk.css.map",
|
||||
"8138.f97957dc.chunk.js.map": "./static/js/8138.f97957dc.chunk.js.map",
|
||||
"1030.986c2667.chunk.js.map": "./static/js/1030.986c2667.chunk.js.map",
|
||||
"9145.363b2352.chunk.js.map": "./static/js/9145.363b2352.chunk.js.map",
|
||||
"1379.0bfc0b60.chunk.js.map": "./static/js/1379.0bfc0b60.chunk.js.map",
|
||||
"1501.c46671fd.chunk.js.map": "./static/js/1501.c46671fd.chunk.js.map",
|
||||
"9605.c3d4a4cf.chunk.js.map": "./static/js/9605.c3d4a4cf.chunk.js.map",
|
||||
"426.e738683c.chunk.js.map": "./static/js/426.e738683c.chunk.js.map",
|
||||
"1379.7cd1ce08.chunk.js.map": "./static/js/1379.7cd1ce08.chunk.js.map",
|
||||
"1501.44151d90.chunk.js.map": "./static/js/1501.44151d90.chunk.js.map",
|
||||
"9605.1542e970.chunk.js.map": "./static/js/9605.1542e970.chunk.js.map",
|
||||
"426.1a73abd5.chunk.js.map": "./static/js/426.1a73abd5.chunk.js.map",
|
||||
"2878.fca6e2cf.chunk.js.map": "./static/js/2878.fca6e2cf.chunk.js.map",
|
||||
"8495.bdd215dc.chunk.js.map": "./static/js/8495.bdd215dc.chunk.js.map",
|
||||
"4934.4a573b0b.chunk.js.map": "./static/js/4934.4a573b0b.chunk.js.map",
|
||||
"3518.e1923a22.chunk.js.map": "./static/js/3518.e1923a22.chunk.js.map",
|
||||
"7021.ea551e6b.chunk.js.map": "./static/js/7021.ea551e6b.chunk.js.map",
|
||||
"2684.569c8172.chunk.js.map": "./static/js/2684.569c8172.chunk.js.map",
|
||||
"6683.53f69e13.chunk.js.map": "./static/js/6683.53f69e13.chunk.js.map",
|
||||
"8350.031612d5.chunk.js.map": "./static/js/8350.031612d5.chunk.js.map",
|
||||
"7021.8869219f.chunk.js.map": "./static/js/7021.8869219f.chunk.js.map",
|
||||
"2684.8bdcdf4b.chunk.js.map": "./static/js/2684.8bdcdf4b.chunk.js.map",
|
||||
"6683.dc4f1821.chunk.js.map": "./static/js/6683.dc4f1821.chunk.js.map",
|
||||
"8350.8e2fd461.chunk.js.map": "./static/js/8350.8e2fd461.chunk.js.map",
|
||||
"2676.bd3d9df3.chunk.js.map": "./static/js/2676.bd3d9df3.chunk.js.map",
|
||||
"9449.83d73a19.chunk.js.map": "./static/js/9449.83d73a19.chunk.js.map",
|
||||
"7659.5154337d.chunk.js.map": "./static/js/7659.5154337d.chunk.js.map",
|
||||
"9968.a7a1674d.chunk.js.map": "./static/js/9968.a7a1674d.chunk.js.map",
|
||||
"2180.c83301fc.chunk.js.map": "./static/js/2180.c83301fc.chunk.js.map",
|
||||
"9449.d160d883.chunk.js.map": "./static/js/9449.d160d883.chunk.js.map",
|
||||
"7659.845e0cad.chunk.js.map": "./static/js/7659.845e0cad.chunk.js.map",
|
||||
"9968.e41064fb.chunk.js.map": "./static/js/9968.e41064fb.chunk.js.map",
|
||||
"2180.ec9a5c77.chunk.js.map": "./static/js/2180.ec9a5c77.chunk.js.map",
|
||||
"8253.964026c0.chunk.js.map": "./static/js/8253.964026c0.chunk.js.map",
|
||||
"3328.da1cf1c8.chunk.js.map": "./static/js/3328.da1cf1c8.chunk.js.map",
|
||||
"3328.c97cfa34.chunk.js.map": "./static/js/3328.c97cfa34.chunk.js.map",
|
||||
"1440.74dce637.chunk.js.map": "./static/js/1440.74dce637.chunk.js.map",
|
||||
"2512.acfc57ce.chunk.js.map": "./static/js/2512.acfc57ce.chunk.js.map",
|
||||
"51.63259724.chunk.js.map": "./static/js/51.63259724.chunk.js.map",
|
||||
"711.5cec9776.chunk.js.map": "./static/js/711.5cec9776.chunk.js.map",
|
||||
"6901.53c6aef7.chunk.js.map": "./static/js/6901.53c6aef7.chunk.js.map",
|
||||
"2185.95c76a1b.chunk.js.map": "./static/js/2185.95c76a1b.chunk.js.map",
|
||||
"312.d183c8e0.chunk.js.map": "./static/js/312.d183c8e0.chunk.js.map",
|
||||
"2112.48f0caa6.chunk.js.map": "./static/js/2112.48f0caa6.chunk.js.map",
|
||||
"4619.f7970b8a.chunk.js.map": "./static/js/4619.f7970b8a.chunk.js.map",
|
||||
"8990.18a36cac.chunk.js.map": "./static/js/8990.18a36cac.chunk.js.map",
|
||||
"8455.0cd71acb.chunk.js.map": "./static/js/8455.0cd71acb.chunk.js.map",
|
||||
"3631.8313f811.chunk.css.map": "./static/css/3631.8313f811.chunk.css.map",
|
||||
"3631.64015ba0.chunk.js.map": "./static/js/3631.64015ba0.chunk.js.map",
|
||||
"2512.a00182cb.chunk.js.map": "./static/js/2512.a00182cb.chunk.js.map",
|
||||
"51.7a2ef89a.chunk.js.map": "./static/js/51.7a2ef89a.chunk.js.map",
|
||||
"711.71917357.chunk.js.map": "./static/js/711.71917357.chunk.js.map",
|
||||
"6901.9f0fcbd5.chunk.js.map": "./static/js/6901.9f0fcbd5.chunk.js.map",
|
||||
"2185.50d8f062.chunk.js.map": "./static/js/2185.50d8f062.chunk.js.map",
|
||||
"312.a4c03743.chunk.js.map": "./static/js/312.a4c03743.chunk.js.map",
|
||||
"2112.4691ccbf.chunk.js.map": "./static/js/2112.4691ccbf.chunk.js.map",
|
||||
"4619.6dd3ad70.chunk.js.map": "./static/js/4619.6dd3ad70.chunk.js.map",
|
||||
"8990.181bff03.chunk.js.map": "./static/js/8990.181bff03.chunk.js.map",
|
||||
"8455.27bfde53.chunk.js.map": "./static/js/8455.27bfde53.chunk.js.map",
|
||||
"3631.04346438.chunk.css.map": "./static/css/3631.04346438.chunk.css.map",
|
||||
"3631.5db9f8b6.chunk.js.map": "./static/js/3631.5db9f8b6.chunk.js.map",
|
||||
"1604.a9d0b62b.chunk.js.map": "./static/js/1604.a9d0b62b.chunk.js.map",
|
||||
"8391.7c39b52d.chunk.js.map": "./static/js/8391.7c39b52d.chunk.js.map",
|
||||
"402.0fe11251.chunk.js.map": "./static/js/402.0fe11251.chunk.js.map",
|
||||
"8391.a94a5da9.chunk.js.map": "./static/js/8391.a94a5da9.chunk.js.map",
|
||||
"402.087a85de.chunk.js.map": "./static/js/402.087a85de.chunk.js.map",
|
||||
"1705.5e57fd31.chunk.js.map": "./static/js/1705.5e57fd31.chunk.js.map",
|
||||
"1581.e5ea40c0.chunk.js.map": "./static/js/1581.e5ea40c0.chunk.js.map",
|
||||
"455.0218ce38.chunk.js.map": "./static/js/455.0218ce38.chunk.js.map",
|
||||
"2661.2121f536.chunk.js.map": "./static/js/2661.2121f536.chunk.js.map",
|
||||
"889.3d385602.chunk.js.map": "./static/js/889.3d385602.chunk.js.map",
|
||||
"9088.e252c094.chunk.js.map": "./static/js/9088.e252c094.chunk.js.map",
|
||||
"247.88de16aa.chunk.js.map": "./static/js/247.88de16aa.chunk.js.map",
|
||||
"2763.44403c2a.chunk.js.map": "./static/js/2763.44403c2a.chunk.js.map",
|
||||
"1581.cbc3b85a.chunk.js.map": "./static/js/1581.cbc3b85a.chunk.js.map",
|
||||
"455.dd2c1a2c.chunk.js.map": "./static/js/455.dd2c1a2c.chunk.js.map",
|
||||
"2661.ffb402c6.chunk.js.map": "./static/js/2661.ffb402c6.chunk.js.map",
|
||||
"889.34a82de2.chunk.js.map": "./static/js/889.34a82de2.chunk.js.map",
|
||||
"9088.edd377c6.chunk.js.map": "./static/js/9088.edd377c6.chunk.js.map",
|
||||
"247.10687506.chunk.js.map": "./static/js/247.10687506.chunk.js.map",
|
||||
"2763.247d377c.chunk.js.map": "./static/js/2763.247d377c.chunk.js.map",
|
||||
"5171.2cf876b1.chunk.js.map": "./static/js/5171.2cf876b1.chunk.js.map",
|
||||
"2426.172b5361.chunk.js.map": "./static/js/2426.172b5361.chunk.js.map",
|
||||
"5561.c5000912.chunk.js.map": "./static/js/5561.c5000912.chunk.js.map",
|
||||
@@ -292,12 +290,12 @@
|
||||
"9785.7ccf0212.chunk.js.map": "./static/js/9785.7ccf0212.chunk.js.map",
|
||||
"8735.52726eac.chunk.js.map": "./static/js/8735.52726eac.chunk.js.map",
|
||||
"63.830fd6fc.chunk.js.map": "./static/js/63.830fd6fc.chunk.js.map",
|
||||
"2983.66cf0ad4.chunk.js.map": "./static/js/2983.66cf0ad4.chunk.js.map",
|
||||
"5289.8c388542.chunk.js.map": "./static/js/5289.8c388542.chunk.js.map",
|
||||
"5026.0b30f6e2.chunk.js.map": "./static/js/5026.0b30f6e2.chunk.js.map"
|
||||
"2983.3e4b1a45.chunk.js.map": "./static/js/2983.3e4b1a45.chunk.js.map",
|
||||
"5289.1434b8bc.chunk.js.map": "./static/js/5289.1434b8bc.chunk.js.map",
|
||||
"5026.ebd99276.chunk.js.map": "./static/js/5026.ebd99276.chunk.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.90d417ae.css",
|
||||
"static/js/main.eec275cb.js"
|
||||
"static/js/main.7be4b5f3.js"
|
||||
]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><base href="/"/><meta content="width=device-width,initial-scale=1" name="viewport"/><meta content="#081C42" media="(prefers-color-scheme: light)" name="theme-color"/><meta content="#081C42" media="(prefers-color-scheme: dark)" name="theme-color"/><meta content="MinIO Console" name="description"/><link href="./styles/root-styles.css" rel="stylesheet"/><link href="./apple-icon-180x180.png" rel="apple-touch-icon" sizes="180x180"/><link href="./favicon-32x32.png" rel="icon" sizes="32x32" type="image/png"/><link href="./favicon-96x96.png" rel="icon" sizes="96x96" type="image/png"/><link href="./favicon-16x16.png" rel="icon" sizes="16x16" type="image/png"/><link href="./manifest.json" rel="manifest"/><link color="#3a4e54" href="./safari-pinned-tab.svg" rel="mask-icon"/><title>MinIO Console</title><script defer="defer" src="./static/js/main.eec275cb.js"></script><link href="./static/css/main.90d417ae.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="preload"><img src="./images/background.svg"/> <img src="./images/background-wave-orig2.svg"/></div><div id="loader-block"><img src="./Loader.svg"/></div></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><base href="/"/><meta content="width=device-width,initial-scale=1" name="viewport"/><meta content="#081C42" media="(prefers-color-scheme: light)" name="theme-color"/><meta content="#081C42" media="(prefers-color-scheme: dark)" name="theme-color"/><meta content="MinIO Console" name="description"/><link href="./styles/root-styles.css" rel="stylesheet"/><link href="./apple-icon-180x180.png" rel="apple-touch-icon" sizes="180x180"/><link href="./favicon-32x32.png" rel="icon" sizes="32x32" type="image/png"/><link href="./favicon-96x96.png" rel="icon" sizes="96x96" type="image/png"/><link href="./favicon-16x16.png" rel="icon" sizes="16x16" type="image/png"/><link href="./manifest.json" rel="manifest"/><link color="#3a4e54" href="./safari-pinned-tab.svg" rel="mask-icon"/><title>MinIO Console</title><script defer="defer" src="./static/js/main.7be4b5f3.js"></script><link href="./static/css/main.90d417ae.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="preload"><img src="./images/background.svg"/> <img src="./images/background-wave-orig2.svg"/></div><div id="loader-block"><img src="./Loader.svg"/></div></div></body></html>
|
||||
@@ -1,2 +1,2 @@
|
||||
.cm-s-dracula .CodeMirror-gutters,.cm-s-dracula.CodeMirror{background-color:#282a36!important;border:none;color:#f8f8f2!important}.cm-s-dracula .CodeMirror-gutters{color:#282a36}.cm-s-dracula .CodeMirror-cursor{border-left:thin solid #f8f8f0}.cm-s-dracula .CodeMirror-linenumber{color:#6d8a88}.cm-s-dracula .CodeMirror-selected{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::selection,.cm-s-dracula .CodeMirror-line>span::selection,.cm-s-dracula .CodeMirror-line>span>span::selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::-moz-selection,.cm-s-dracula .CodeMirror-line>span::-moz-selection,.cm-s-dracula .CodeMirror-line>span>span::-moz-selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula span.cm-comment{color:#6272a4}.cm-s-dracula span.cm-string,.cm-s-dracula span.cm-string-2{color:#f1fa8c}.cm-s-dracula span.cm-number{color:#bd93f9}.cm-s-dracula span.cm-variable{color:#50fa7b}.cm-s-dracula span.cm-variable-2{color:#fff}.cm-s-dracula span.cm-def{color:#50fa7b}.cm-s-dracula span.cm-keyword,.cm-s-dracula span.cm-operator{color:#ff79c6}.cm-s-dracula span.cm-atom{color:#bd93f9}.cm-s-dracula span.cm-meta{color:#f8f8f2}.cm-s-dracula span.cm-tag{color:#ff79c6}.cm-s-dracula span.cm-attribute,.cm-s-dracula span.cm-qualifier{color:#50fa7b}.cm-s-dracula span.cm-property{color:#66d9ef}.cm-s-dracula span.cm-builtin{color:#50fa7b}.cm-s-dracula span.cm-type,.cm-s-dracula span.cm-variable-3{color:#ffb86c}.cm-s-dracula .CodeMirror-activeline-background{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-matchingbracket{color:#fff!important;text-decoration:underline}
|
||||
/*# sourceMappingURL=1955.8313f811.chunk.css.map*/
|
||||
/*# sourceMappingURL=1955.04346438.chunk.css.map*/
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"static/css/1955.8313f811.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}
|
||||
{"version":3,"file":"static/css/1955.04346438.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}
|
||||
@@ -1,2 +1,2 @@
|
||||
.cm-s-dracula .CodeMirror-gutters,.cm-s-dracula.CodeMirror{background-color:#282a36!important;border:none;color:#f8f8f2!important}.cm-s-dracula .CodeMirror-gutters{color:#282a36}.cm-s-dracula .CodeMirror-cursor{border-left:thin solid #f8f8f0}.cm-s-dracula .CodeMirror-linenumber{color:#6d8a88}.cm-s-dracula .CodeMirror-selected{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::selection,.cm-s-dracula .CodeMirror-line>span::selection,.cm-s-dracula .CodeMirror-line>span>span::selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::-moz-selection,.cm-s-dracula .CodeMirror-line>span::-moz-selection,.cm-s-dracula .CodeMirror-line>span>span::-moz-selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula span.cm-comment{color:#6272a4}.cm-s-dracula span.cm-string,.cm-s-dracula span.cm-string-2{color:#f1fa8c}.cm-s-dracula span.cm-number{color:#bd93f9}.cm-s-dracula span.cm-variable{color:#50fa7b}.cm-s-dracula span.cm-variable-2{color:#fff}.cm-s-dracula span.cm-def{color:#50fa7b}.cm-s-dracula span.cm-keyword,.cm-s-dracula span.cm-operator{color:#ff79c6}.cm-s-dracula span.cm-atom{color:#bd93f9}.cm-s-dracula span.cm-meta{color:#f8f8f2}.cm-s-dracula span.cm-tag{color:#ff79c6}.cm-s-dracula span.cm-attribute,.cm-s-dracula span.cm-qualifier{color:#50fa7b}.cm-s-dracula span.cm-property{color:#66d9ef}.cm-s-dracula span.cm-builtin{color:#50fa7b}.cm-s-dracula span.cm-type,.cm-s-dracula span.cm-variable-3{color:#ffb86c}.cm-s-dracula .CodeMirror-activeline-background{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-matchingbracket{color:#fff!important;text-decoration:underline}
|
||||
/*# sourceMappingURL=2080.8313f811.chunk.css.map*/
|
||||
/*# sourceMappingURL=2080.04346438.chunk.css.map*/
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"static/css/2080.8313f811.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}
|
||||
{"version":3,"file":"static/css/2080.04346438.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}
|
||||
@@ -1,2 +1,2 @@
|
||||
.cm-s-dracula .CodeMirror-gutters,.cm-s-dracula.CodeMirror{background-color:#282a36!important;border:none;color:#f8f8f2!important}.cm-s-dracula .CodeMirror-gutters{color:#282a36}.cm-s-dracula .CodeMirror-cursor{border-left:thin solid #f8f8f0}.cm-s-dracula .CodeMirror-linenumber{color:#6d8a88}.cm-s-dracula .CodeMirror-selected{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::selection,.cm-s-dracula .CodeMirror-line>span::selection,.cm-s-dracula .CodeMirror-line>span>span::selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::-moz-selection,.cm-s-dracula .CodeMirror-line>span::-moz-selection,.cm-s-dracula .CodeMirror-line>span>span::-moz-selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula span.cm-comment{color:#6272a4}.cm-s-dracula span.cm-string,.cm-s-dracula span.cm-string-2{color:#f1fa8c}.cm-s-dracula span.cm-number{color:#bd93f9}.cm-s-dracula span.cm-variable{color:#50fa7b}.cm-s-dracula span.cm-variable-2{color:#fff}.cm-s-dracula span.cm-def{color:#50fa7b}.cm-s-dracula span.cm-keyword,.cm-s-dracula span.cm-operator{color:#ff79c6}.cm-s-dracula span.cm-atom{color:#bd93f9}.cm-s-dracula span.cm-meta{color:#f8f8f2}.cm-s-dracula span.cm-tag{color:#ff79c6}.cm-s-dracula span.cm-attribute,.cm-s-dracula span.cm-qualifier{color:#50fa7b}.cm-s-dracula span.cm-property{color:#66d9ef}.cm-s-dracula span.cm-builtin{color:#50fa7b}.cm-s-dracula span.cm-type,.cm-s-dracula span.cm-variable-3{color:#ffb86c}.cm-s-dracula .CodeMirror-activeline-background{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-matchingbracket{color:#fff!important;text-decoration:underline}
|
||||
/*# sourceMappingURL=3368.8313f811.chunk.css.map*/
|
||||
/*# sourceMappingURL=3631.04346438.chunk.css.map*/
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"static/css/3368.8313f811.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}
|
||||
{"version":3,"file":"static/css/3631.04346438.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}
|
||||
@@ -1,2 +0,0 @@
|
||||
.cm-s-dracula .CodeMirror-gutters,.cm-s-dracula.CodeMirror{background-color:#282a36!important;border:none;color:#f8f8f2!important}.cm-s-dracula .CodeMirror-gutters{color:#282a36}.cm-s-dracula .CodeMirror-cursor{border-left:thin solid #f8f8f0}.cm-s-dracula .CodeMirror-linenumber{color:#6d8a88}.cm-s-dracula .CodeMirror-selected{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::selection,.cm-s-dracula .CodeMirror-line>span::selection,.cm-s-dracula .CodeMirror-line>span>span::selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::-moz-selection,.cm-s-dracula .CodeMirror-line>span::-moz-selection,.cm-s-dracula .CodeMirror-line>span>span::-moz-selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula span.cm-comment{color:#6272a4}.cm-s-dracula span.cm-string,.cm-s-dracula span.cm-string-2{color:#f1fa8c}.cm-s-dracula span.cm-number{color:#bd93f9}.cm-s-dracula span.cm-variable{color:#50fa7b}.cm-s-dracula span.cm-variable-2{color:#fff}.cm-s-dracula span.cm-def{color:#50fa7b}.cm-s-dracula span.cm-keyword,.cm-s-dracula span.cm-operator{color:#ff79c6}.cm-s-dracula span.cm-atom{color:#bd93f9}.cm-s-dracula span.cm-meta{color:#f8f8f2}.cm-s-dracula span.cm-tag{color:#ff79c6}.cm-s-dracula span.cm-attribute,.cm-s-dracula span.cm-qualifier{color:#50fa7b}.cm-s-dracula span.cm-property{color:#66d9ef}.cm-s-dracula span.cm-builtin{color:#50fa7b}.cm-s-dracula span.cm-type,.cm-s-dracula span.cm-variable-3{color:#ffb86c}.cm-s-dracula .CodeMirror-activeline-background{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-matchingbracket{color:#fff!important;text-decoration:underline}
|
||||
/*# sourceMappingURL=3688.8313f811.chunk.css.map*/
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"static/css/3688.8313f811.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}
|
||||
@@ -1,2 +1,2 @@
|
||||
.cm-s-dracula .CodeMirror-gutters,.cm-s-dracula.CodeMirror{background-color:#282a36!important;border:none;color:#f8f8f2!important}.cm-s-dracula .CodeMirror-gutters{color:#282a36}.cm-s-dracula .CodeMirror-cursor{border-left:thin solid #f8f8f0}.cm-s-dracula .CodeMirror-linenumber{color:#6d8a88}.cm-s-dracula .CodeMirror-selected{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::selection,.cm-s-dracula .CodeMirror-line>span::selection,.cm-s-dracula .CodeMirror-line>span>span::selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::-moz-selection,.cm-s-dracula .CodeMirror-line>span::-moz-selection,.cm-s-dracula .CodeMirror-line>span>span::-moz-selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula span.cm-comment{color:#6272a4}.cm-s-dracula span.cm-string,.cm-s-dracula span.cm-string-2{color:#f1fa8c}.cm-s-dracula span.cm-number{color:#bd93f9}.cm-s-dracula span.cm-variable{color:#50fa7b}.cm-s-dracula span.cm-variable-2{color:#fff}.cm-s-dracula span.cm-def{color:#50fa7b}.cm-s-dracula span.cm-keyword,.cm-s-dracula span.cm-operator{color:#ff79c6}.cm-s-dracula span.cm-atom{color:#bd93f9}.cm-s-dracula span.cm-meta{color:#f8f8f2}.cm-s-dracula span.cm-tag{color:#ff79c6}.cm-s-dracula span.cm-attribute,.cm-s-dracula span.cm-qualifier{color:#50fa7b}.cm-s-dracula span.cm-property{color:#66d9ef}.cm-s-dracula span.cm-builtin{color:#50fa7b}.cm-s-dracula span.cm-type,.cm-s-dracula span.cm-variable-3{color:#ffb86c}.cm-s-dracula .CodeMirror-activeline-background{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-matchingbracket{color:#fff!important;text-decoration:underline}
|
||||
/*# sourceMappingURL=380.8313f811.chunk.css.map*/
|
||||
/*# sourceMappingURL=380.04346438.chunk.css.map*/
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"static/css/380.8313f811.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}
|
||||
{"version":3,"file":"static/css/380.04346438.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}
|
||||
@@ -1,2 +1,2 @@
|
||||
.cm-s-dracula .CodeMirror-gutters,.cm-s-dracula.CodeMirror{background-color:#282a36!important;border:none;color:#f8f8f2!important}.cm-s-dracula .CodeMirror-gutters{color:#282a36}.cm-s-dracula .CodeMirror-cursor{border-left:thin solid #f8f8f0}.cm-s-dracula .CodeMirror-linenumber{color:#6d8a88}.cm-s-dracula .CodeMirror-selected{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::selection,.cm-s-dracula .CodeMirror-line>span::selection,.cm-s-dracula .CodeMirror-line>span>span::selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::-moz-selection,.cm-s-dracula .CodeMirror-line>span::-moz-selection,.cm-s-dracula .CodeMirror-line>span>span::-moz-selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula span.cm-comment{color:#6272a4}.cm-s-dracula span.cm-string,.cm-s-dracula span.cm-string-2{color:#f1fa8c}.cm-s-dracula span.cm-number{color:#bd93f9}.cm-s-dracula span.cm-variable{color:#50fa7b}.cm-s-dracula span.cm-variable-2{color:#fff}.cm-s-dracula span.cm-def{color:#50fa7b}.cm-s-dracula span.cm-keyword,.cm-s-dracula span.cm-operator{color:#ff79c6}.cm-s-dracula span.cm-atom{color:#bd93f9}.cm-s-dracula span.cm-meta{color:#f8f8f2}.cm-s-dracula span.cm-tag{color:#ff79c6}.cm-s-dracula span.cm-attribute,.cm-s-dracula span.cm-qualifier{color:#50fa7b}.cm-s-dracula span.cm-property{color:#66d9ef}.cm-s-dracula span.cm-builtin{color:#50fa7b}.cm-s-dracula span.cm-type,.cm-s-dracula span.cm-variable-3{color:#ffb86c}.cm-s-dracula .CodeMirror-activeline-background{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-matchingbracket{color:#fff!important;text-decoration:underline}
|
||||
/*# sourceMappingURL=3631.8313f811.chunk.css.map*/
|
||||
/*# sourceMappingURL=6633.04346438.chunk.css.map*/
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"static/css/3631.8313f811.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}
|
||||
{"version":3,"file":"static/css/6633.04346438.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}
|
||||
2
portal-ui/build/static/css/6859.04346438.chunk.css
Normal file
2
portal-ui/build/static/css/6859.04346438.chunk.css
Normal file
@@ -0,0 +1,2 @@
|
||||
.cm-s-dracula .CodeMirror-gutters,.cm-s-dracula.CodeMirror{background-color:#282a36!important;border:none;color:#f8f8f2!important}.cm-s-dracula .CodeMirror-gutters{color:#282a36}.cm-s-dracula .CodeMirror-cursor{border-left:thin solid #f8f8f0}.cm-s-dracula .CodeMirror-linenumber{color:#6d8a88}.cm-s-dracula .CodeMirror-selected{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::selection,.cm-s-dracula .CodeMirror-line>span::selection,.cm-s-dracula .CodeMirror-line>span>span::selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-line::-moz-selection,.cm-s-dracula .CodeMirror-line>span::-moz-selection,.cm-s-dracula .CodeMirror-line>span>span::-moz-selection{background:hsla(0,0%,100%,.1)}.cm-s-dracula span.cm-comment{color:#6272a4}.cm-s-dracula span.cm-string,.cm-s-dracula span.cm-string-2{color:#f1fa8c}.cm-s-dracula span.cm-number{color:#bd93f9}.cm-s-dracula span.cm-variable{color:#50fa7b}.cm-s-dracula span.cm-variable-2{color:#fff}.cm-s-dracula span.cm-def{color:#50fa7b}.cm-s-dracula span.cm-keyword,.cm-s-dracula span.cm-operator{color:#ff79c6}.cm-s-dracula span.cm-atom{color:#bd93f9}.cm-s-dracula span.cm-meta{color:#f8f8f2}.cm-s-dracula span.cm-tag{color:#ff79c6}.cm-s-dracula span.cm-attribute,.cm-s-dracula span.cm-qualifier{color:#50fa7b}.cm-s-dracula span.cm-property{color:#66d9ef}.cm-s-dracula span.cm-builtin{color:#50fa7b}.cm-s-dracula span.cm-type,.cm-s-dracula span.cm-variable-3{color:#ffb86c}.cm-s-dracula .CodeMirror-activeline-background{background:hsla(0,0%,100%,.1)}.cm-s-dracula .CodeMirror-matchingbracket{color:#fff!important;text-decoration:underline}
|
||||
/*# sourceMappingURL=6859.04346438.chunk.css.map*/
|
||||
1
portal-ui/build/static/css/6859.04346438.chunk.css.map
Normal file
1
portal-ui/build/static/css/6859.04346438.chunk.css.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"static/css/6859.04346438.chunk.css","mappings":"AAUA,2DACE,kCAAoC,CAEpC,WAAY,CADZ,uBAEF,CACA,kCAAoC,aAAgB,CACpD,iCAAmC,8BAAiC,CACpE,qCAAuC,aAAgB,CACvD,mCAAqC,6BAAuC,CAC5E,6IAAuJ,6BAAuC,CAC9L,4JAAsK,6BAAuC,CAC7M,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAC/E,6BAA+B,aAAgB,CAC/C,+BAAiC,aAAgB,CACjD,iCAAmC,UAAc,CACjD,0BAA4B,aAAgB,CAE5C,6DAAgC,aAAgB,CAChD,2BAA6B,aAAgB,CAC7C,2BAA6B,aAAgB,CAC7C,0BAA4B,aAAgB,CAE5C,gEAAkC,aAAgB,CAClD,+BAAiC,aAAgB,CACjD,8BAAgC,aAAgB,CAChD,4DAA+D,aAAgB,CAE/E,gDAAkD,6BAAmC,CACrF,0CAAwE,oBAAuB,CAAnD,yBAAqD","sources":["../node_modules/codemirror/theme/dracula.css"],"sourcesContent":["/*\n\n Name: dracula\n Author: Michael Kaminsky (http://github.com/mkaminsky11)\n\n Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)\n\n*/\n\n\n.cm-s-dracula.CodeMirror, .cm-s-dracula .CodeMirror-gutters {\n background-color: #282a36 !important;\n color: #f8f8f2 !important;\n border: none;\n}\n.cm-s-dracula .CodeMirror-gutters { color: #282a36; }\n.cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; }\n.cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; }\n.cm-s-dracula .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }\n.cm-s-dracula span.cm-comment { color: #6272a4; }\n.cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; }\n.cm-s-dracula span.cm-number { color: #bd93f9; }\n.cm-s-dracula span.cm-variable { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-2 { color: white; }\n.cm-s-dracula span.cm-def { color: #50fa7b; }\n.cm-s-dracula span.cm-operator { color: #ff79c6; }\n.cm-s-dracula span.cm-keyword { color: #ff79c6; }\n.cm-s-dracula span.cm-atom { color: #bd93f9; }\n.cm-s-dracula span.cm-meta { color: #f8f8f2; }\n.cm-s-dracula span.cm-tag { color: #ff79c6; }\n.cm-s-dracula span.cm-attribute { color: #50fa7b; }\n.cm-s-dracula span.cm-qualifier { color: #50fa7b; }\n.cm-s-dracula span.cm-property { color: #66d9ef; }\n.cm-s-dracula span.cm-builtin { color: #50fa7b; }\n.cm-s-dracula span.cm-variable-3, .cm-s-dracula span.cm-type { color: #ffb86c; }\n\n.cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); }\n.cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }\n"],"names":[],"sourceRoot":""}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user