From fa1233944c6f38d6a3b68cfc9b242d0282fd2b1f Mon Sep 17 00:00:00 2001 From: Nikita Akilov <26031301+n5a5@users.noreply.github.com> Date: Wed, 2 Jul 2025 20:26:04 +0200 Subject: [PATCH] dbeaver/pro#6164 headless configuration with go --- .env.template | 2 + go/api/client.go | 77 ++++++++++++++++++++++++++++ go/main.go | 76 +++++++++------------------ go/scenario0.go | 44 ++++++++++++++++ go/scenario1.go | 38 ++++++++++++++ operations/generic_auth.gql | 5 ++ operations/import_license.gql | 20 ++++++++ operations/initial_configuration.gql | 3 ++ 8 files changed, 214 insertions(+), 51 deletions(-) create mode 100644 go/scenario0.go create mode 100644 go/scenario1.go create mode 100644 operations/generic_auth.gql create mode 100644 operations/import_license.gql create mode 100644 operations/initial_configuration.gql diff --git a/.env.template b/.env.template index 9ce514e..cb0f337 100644 --- a/.env.template +++ b/.env.template @@ -8,3 +8,5 @@ server_url= # `api` by default. # See `serviceURI` field here: https://github.com/dbeaver/cloudbeaver/wiki/Server-configuration service_uri=api + +license_text= diff --git a/go/api/client.go b/go/api/client.go index 21a6ac9..9dfc0b1 100644 --- a/go/api/client.go +++ b/go/api/client.go @@ -1,11 +1,14 @@ package api import ( + "crypto/md5" + "encoding/hex" "encoding/json" "errors" "fmt" "log/slog" "os" + "strings" "time" "github.com/dbeaver/cloudbeaver-graphql-examples/go/graphql" @@ -14,6 +17,7 @@ import ( type Client struct { GraphQLClient graphql.Client + ServerURL string Endpoint string OperationsPath string } @@ -61,6 +65,23 @@ func (client Client) Auth(token string) error { return client.sendRequestDiscardingData("auth", query, variables) } +func (client Client) LocalAuth(username, password string) error { + query, err := client.readOperationText("generic_auth") + if err != nil { + return err + } + rawMD5Sum := md5.Sum([]byte(password)) + passwordHash := strings.ToUpper(hex.EncodeToString(rawMD5Sum[:])) + variables := map[string]any{ + "provider": "local", + "credentials": map[string]any{ + "user": username, + "password": passwordHash, + }, + } + return client.sendRequestDiscardingData("local auth", query, variables) +} + func (client Client) CreateTeam(teamId string) error { query, err := client.readOperationText("create_team") if err != nil { @@ -151,3 +172,59 @@ func (client Client) AddProjectAccess(projectId string, subjectIds ...string) er variables, ) } + +func (client Client) ImportLicense(licenseText string) ([]byte, error) { + query, err := client.readOperationText("import_license") + if err != nil { + return []byte{}, err + } + variables := map[string]any{ + "licenseText": licenseText, + } + return client.sendRequest( + "importing a license", + query, + variables, + ) +} + +func (client Client) InitialConfiguration(username, password, serverName string) ([]byte, error) { + query, err := client.readOperationText("initial_configuration") + if err != nil { + return []byte{}, err + } + variables := map[string]any{ + "configuration": map[string]any{ + "adminName": username, + "adminPassword": password, + "serverName": serverName, + "sessionExpireTime": 694201337, + "adminCredentialsSaveEnabled": false, + "publicCredentialsSaveEnabled": false, + "customConnectionsEnabled": false, + "disabledDrivers": [6]string{ + "sqlite-ee:sqlite_ee", + "sqlite-crypt:sqlite_crypt", + "h2:h2_embedded", + "h2:h2_embedded_v2", + "generic:duckdb_jdbc", + "h2gis:h2gis_embedded", + }, + "enabledAuthProviders": [4]string{ + "local", + "token", + "openid", + "okta-openid", + }, + "anonymousAccessEnabled": false, + "enabledFeatures": [0]string{}, + "resourceManagerEnabled": true, + "secretManagerEnabled": false, + }, + } + return client.sendRequest( + "doing initial configuration", + query, + variables, + ) +} diff --git a/go/main.go b/go/main.go index da5c3d1..fb5e5de 100644 --- a/go/main.go +++ b/go/main.go @@ -22,13 +22,13 @@ const ( ) func main() { - if err := main0(); err != nil { + if err := run(); err != nil { slog.Error(err.Error()) os.Exit(1) } } -func main0() error { +func run() error { // Instantiate a client envFlag := flag.String("env", "../.env", "Path to the .env file") operationsFlag := flag.String("operations", "../operations", "Path to the folder with GraphQL operations") @@ -37,45 +37,31 @@ func main0() error { if err != nil { return lib.WrapError("error while reading variables", err) } - apiClient := initClient(env.serverURL+"/"+env.serviceURI+"/gql", *operationsFlag) - - // Auth - err = apiClient.Auth(env.apiToken) - if err != nil { - return err - } - - // Creation / deletion of a team - err = apiClient.CreateTeam(teamId) + cookieJar, err := cookiejar.New(nil) if err != nil { - return err + // Invariant: the method that creates cookie jar with no options never returns non-nil err + panic("encountered error while creating a cookie jar! " + err.Error()) } - defer cleanup("delete team "+teamId, func() error { - return apiClient.DeleteTeam(teamId, true) - }) - - // Creation of a project - projectId, err := apiClient.CreateProject(projectName) - if err != nil { - return err + graphQLClient := graphql.Client{HttpClient: &http.Client{Jar: cookieJar}} + apiClient := api.Client{ + GraphQLClient: graphQLClient, + ServerURL: env.serverURL, + Endpoint: env.serverURL + "/" + env.serviceURI + "/gql", + OperationsPath: *operationsFlag, } - defer cleanup("delete project "+projectId, func() error { - return apiClient.DeleteProject(projectId) - }) - // Grant access - err = apiClient.AddProjectAccess(projectId, teamId) - if err != nil { + // Run scenarios + if err := scenario0(&apiClient, env.apiToken); err != nil { return err } - - return nil + return scenario1(&apiClient, env.licenseText) } type env struct { - apiToken string - serverURL string - serviceURI string + apiToken string + serverURL string + serviceURI string + licenseText string } func readEnv(envFilePath string) (env, error) { @@ -87,7 +73,11 @@ func readEnv(envFilePath string) (env, error) { defer lib.CloseOrWarn(file) scanner := bufio.NewScanner(file) for scanner.Scan() { - before, after, found := strings.Cut(scanner.Text(), "=") + line := scanner.Text() + if strings.HasPrefix(line, "#") { + continue + } + before, after, found := strings.Cut(line, "=") if !found { continue } @@ -100,27 +90,11 @@ func readEnv(envFilePath string) (env, error) { env.serverURL = after case "service_uri": env.serviceURI = after + case "license_text": + env.licenseText = after default: slog.Warn(fmt.Sprintf("unknown env variable: %s", before)) } } return env, err } - -func initClient(endpoint string, operationsPath string) api.Client { - cookieJar, err := cookiejar.New(nil) - if err != nil { - // Invariant: the method that creates cookie jar with no options never returns non-nil err - panic("encountered error while creating a cookie jar! " + err.Error()) - } - graphQLClient := graphql.Client{HttpClient: &http.Client{Jar: cookieJar}} - return api.Client{GraphQLClient: graphQLClient, Endpoint: endpoint, OperationsPath: operationsPath} -} - -func cleanup(callDescription string, apiCall func() error) { - err := apiCall() - if err != nil { - slog.Warn("unable to " + callDescription) - slog.Warn(err.Error()) - } -} diff --git a/go/scenario0.go b/go/scenario0.go new file mode 100644 index 0000000..c4ce6e9 --- /dev/null +++ b/go/scenario0.go @@ -0,0 +1,44 @@ +package main + +import ( + "log/slog" + + "github.com/dbeaver/cloudbeaver-graphql-examples/go/api" +) + +func scenario0(apiClient *api.Client, token string) error { + // Auth w/ API token + err := apiClient.Auth(token) + if err != nil { + return err + } + + // Creation / deletion of a team + err = apiClient.CreateTeam(teamId) + if err != nil { + return err + } + defer cleanup("delete team "+teamId, func() error { + return apiClient.DeleteTeam(teamId, true) + }) + + // Creation of a project + projectId, err := apiClient.CreateProject(projectName) + if err != nil { + return err + } + defer cleanup("delete project "+projectId, func() error { + return apiClient.DeleteProject(projectId) + }) + + // Grant access + return apiClient.AddProjectAccess(projectId, teamId) +} + +func cleanup(callDescription string, apiCall func() error) { + err := apiCall() + if err != nil { + slog.Warn("unable to " + callDescription) + slog.Warn(err.Error()) + } +} diff --git a/go/scenario1.go b/go/scenario1.go new file mode 100644 index 0000000..a4112ea --- /dev/null +++ b/go/scenario1.go @@ -0,0 +1,38 @@ +package main + +import ( + "crypto/tls" + "fmt" + "net/http" + + "github.com/dbeaver/cloudbeaver-graphql-examples/go/api" +) + +// This scenario is used to perform initial configuration of the server, such as setting up the license and local admin. +func scenario1(apiClient *api.Client, licenseText string) error { + username := "teadmin" + password := "VeryStr0ngPassw0rd" + + // USE WITH CAUTION: this disables TLS certt verification, which is not recommended for production use + if false { + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + } + + rawResponse, err := apiClient.InitialConfiguration(username, password, objectPrefix+"server-name") + if err != nil { + return err + } + fmt.Println(string(rawResponse)) + + if err := apiClient.LocalAuth(username, password); err != nil { + return err + } + + rawResponse, err = apiClient.ImportLicense(licenseText) + if err != nil { + return err + } + fmt.Println(string(rawResponse)) + + return nil +} diff --git a/operations/generic_auth.gql b/operations/generic_auth.gql new file mode 100644 index 0000000..5a5f177 --- /dev/null +++ b/operations/generic_auth.gql @@ -0,0 +1,5 @@ +query genericAuth($provider: ID!, $credentials: Object!) { + authLogin(provider: $provider, credentials: $credentials) { + authStatus + } +} diff --git a/operations/import_license.gql b/operations/import_license.gql new file mode 100644 index 0000000..a78f053 --- /dev/null +++ b/operations/import_license.gql @@ -0,0 +1,20 @@ +mutation importProductLicense($licenseText: String) { + license: importProductLicense(licenseText: $licenseText) { + licenseType + ownerCompany + ownerName + ownerEmail + yearsNumber + subscription + multiInstance + unlimitedServers + licenseIssueTime + licenseStartTime + licenseEndTime + serversNumber + licenseRoles { + role + usersNumber + } + } +} diff --git a/operations/initial_configuration.gql b/operations/initial_configuration.gql new file mode 100644 index 0000000..fc0897f --- /dev/null +++ b/operations/initial_configuration.gql @@ -0,0 +1,3 @@ +query configureServer($configuration: ServerConfigInput!) { + configureServer(configuration: $configuration) +}