Files
akvorado/orchestrator/clickhouse/migrations.go
Vincent Bernat 7d1ba478a1 orchestrator/clickhouse: rework migrations to use an abstract schema
We introduce an leaky abstraction for flows schema and use it for
migrations as a first step.

For views and dictionaries, we stop relying on a hash to know if they
need to be recreated, but we compare the select statements with our
target statement. This is a bit fragile, but strictly better than the
hash.

For data tables, we add the missing columns.

We give up on the abstraction of a migration step and just rely on
helper functions to get the same result. The migration code is now
shorter and we don't need to update it when adding new columns.

This is a preparatory work for #211 to allow a user to specify
additional fields to collect.
2023-01-02 23:42:05 +01:00

140 lines
3.7 KiB
Go

// SPDX-FileCopyrightText: 2022 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only
package clickhouse
import (
"context"
"fmt"
"net"
"github.com/ClickHouse/clickhouse-go/v2"
"akvorado/common/reporter"
)
type migrationStep struct {
// CheckQuery to execute to check if the step is needed.
CheckQuery string
// Arguments to use for the query
Args []interface{}
// Function to execute if the query returns no row or returns `0'.
Do func() error
}
type migrationStepFunc func(context.Context, reporter.Logger, clickhouse.Conn) migrationStep
type migrationStepWithDescription struct {
Description string
Step migrationStepFunc
}
// migrateDatabase execute database migration
func (c *Component) migrateDatabase() error {
ctx := c.t.Context(nil)
// Set orchestrator URL
if c.config.OrchestratorURL == "" {
baseURL, err := c.getHTTPBaseURL("1.1.1.1:80")
if err != nil {
return err
}
c.config.OrchestratorURL = baseURL
}
// Limit number of consumers to the number of threads
row := c.d.ClickHouse.QueryRow(ctx, `SELECT getSetting('max_threads')`)
if err := row.Err(); err != nil {
c.r.Err(err).Msg("unable to query database")
return fmt.Errorf("unable to query database: %w", err)
}
var threads uint8
if err := row.Scan(&threads); err != nil {
c.r.Err(err).Msg("unable to parse number of threads")
return fmt.Errorf("unable to parse number of threads: %w", err)
}
if c.config.Kafka.Consumers > int(threads) {
c.r.Warn().Msgf("too many consumers requested, capping to %d", threads)
c.config.Kafka.Consumers = int(threads)
}
// Create dictionaries
err := c.wrapMigrations(
func() error {
return c.createDictionary(ctx, "asns", "hashed",
"`asn` UInt32 INJECTIVE, `name` String", "asn")
}, func() error {
return c.createDictionary(ctx, "protocols", "hashed",
"`proto` UInt8 INJECTIVE, `name` String, `description` String", "proto")
}, func() error {
return c.createDictionary(ctx, "networks", "ip_trie",
"`network` String, `name` String, `role` String, `site` String, `region` String, `tenant` String",
"network")
})
if err != nil {
return err
}
// Create the various non-raw flow tables
for _, resolution := range c.config.Resolutions {
err := c.wrapMigrations(
func() error {
return c.createOrUpdateFlowsTable(ctx, resolution)
}, func() error {
return c.createFlowsConsumerView(ctx, resolution)
})
if err != nil {
return err
}
}
// Remaining tables
err = c.wrapMigrations(
func() error {
return c.createExportersView(ctx)
}, func() error {
return c.createRawFlowsTable(ctx)
}, func() error {
return c.createRawFlowsConsumerView(ctx)
}, func() error {
return c.createRawFlowsErrorsView(ctx)
},
)
if err != nil {
return err
}
close(c.migrationsDone)
c.metrics.migrationsRunning.Set(0)
// Reload dictionaries
if err := c.d.ClickHouse.Exec(ctx, "SYSTEM RELOAD DICTIONARIES"); err != nil {
c.r.Err(err).Msg("unable to reload dictionaries after migration")
}
return nil
}
// getHTTPBaseURL tries to guess the appropriate URL to access our
// HTTP daemon. It tries to get our IP address using an unconnected
// UDP socket.
func (c *Component) getHTTPBaseURL(address string) (string, error) {
// Get IP address
conn, err := net.Dial("udp", address)
if err != nil {
return "", fmt.Errorf("cannot get our IP address: %w", err)
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
// Combine with HTTP port
_, port, err := net.SplitHostPort(c.d.HTTP.LocalAddr().String())
if err != nil {
return "", fmt.Errorf("cannot get HTTP port: %w", err)
}
base := fmt.Sprintf("http://%s",
net.JoinHostPort(localAddr.IP.String(), port))
c.r.Debug().Msgf("detected base URL is %s", base)
return base, nil
}