mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
lib/promscrape/discovery/kubernetes: follow-up after 0b5c874911
(#2672)
This commit is contained in:
parent
006b8c7534
commit
a18914abee
12 changed files with 404 additions and 245 deletions
|
@ -15,6 +15,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
|
||||||
|
|
||||||
## tip
|
## tip
|
||||||
|
|
||||||
|
* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): Allows using kubeconfig file within `kubernetes_sd_configs`. It may be useful for kubernetes cluster monitoring by `vmagent` outside kubernetes cluster. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1464).
|
||||||
* FEATURE: allow overriding default limits for in-memory cache `indexdb/tagFilters` via flag `-storage.cacheSizeIndexDBTagFilters`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2663).
|
* FEATURE: allow overriding default limits for in-memory cache `indexdb/tagFilters` via flag `-storage.cacheSizeIndexDBTagFilters`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2663).
|
||||||
* FEATURE: add support of `lowercase` and `uppercase` relabeling actions in the same way as [Prometheus 2.36.0 does](https://github.com/prometheus/prometheus/releases/tag/v2.36.0). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2664).
|
* FEATURE: add support of `lowercase` and `uppercase` relabeling actions in the same way as [Prometheus 2.36.0 does](https://github.com/prometheus/prometheus/releases/tag/v2.36.0). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2664).
|
||||||
* FEATURE: support query tracing, which allows determining bottlenecks during query processing. See [these docs](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#query-tracing) and [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1403).
|
* FEATURE: support query tracing, which allows determining bottlenecks during query processing. See [these docs](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#query-tracing) and [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1403).
|
||||||
|
|
|
@ -470,13 +470,12 @@ func NewConfig(baseDir string, az *Authorization, basicAuth *BasicAuthConfig, be
|
||||||
return nil, fmt.Errorf("cannot load TLS certificate from `cert_file`=%q, `key_file`=%q: %w", tlsConfig.CertFile, tlsConfig.KeyFile, err)
|
return nil, fmt.Errorf("cannot load TLS certificate from `cert_file`=%q, `key_file`=%q: %w", tlsConfig.CertFile, tlsConfig.KeyFile, err)
|
||||||
}
|
}
|
||||||
return &cert, nil
|
return &cert, nil
|
||||||
} else {
|
|
||||||
cert, err := tls.X509KeyPair(tlsConfig.Cert, tlsConfig.Key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot load TLS certificate: %w", err)
|
|
||||||
}
|
|
||||||
return &cert, nil
|
|
||||||
}
|
}
|
||||||
|
cert, err := tls.X509KeyPair(tlsConfig.Cert, tlsConfig.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot load TLS certificate: %w", err)
|
||||||
|
}
|
||||||
|
return &cert, nil
|
||||||
}
|
}
|
||||||
// Check whether the configured TLS cert can be loaded.
|
// Check whether the configured TLS cert can be loaded.
|
||||||
if _, err := getTLSCert(nil); err != nil {
|
if _, err := getTLSCert(nil); err != nil {
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
package kubernetes
|
package kubernetes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -12,164 +9,6 @@ import (
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
// apiConfig contains config for API server
|
|
||||||
type apiConfig struct {
|
|
||||||
aw *apiWatcher
|
|
||||||
}
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Kind string `yaml:"kind,omitempty"`
|
|
||||||
APIVersion string `yaml:"apiVersion,omitempty"`
|
|
||||||
Preferences Preferences `yaml:"preferences"`
|
|
||||||
Clusters []struct {
|
|
||||||
Name string `yaml:"name"`
|
|
||||||
Cluster *Cluster `yaml:"cluster"`
|
|
||||||
} `yaml:"clusters"`
|
|
||||||
AuthInfos []struct {
|
|
||||||
Name string `yaml:"name"`
|
|
||||||
AuthInfo *AuthInfo `yaml:"user"`
|
|
||||||
} `yaml:"users"`
|
|
||||||
Contexts []struct {
|
|
||||||
Name string `yaml:"name"`
|
|
||||||
Context *Context `yaml:"context"`
|
|
||||||
} `yaml:"contexts"`
|
|
||||||
CurrentContext string `yaml:"current-context"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Preferences struct {
|
|
||||||
Colors bool `yaml:"colors,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Cluster struct {
|
|
||||||
LocationOfOrigin string
|
|
||||||
Server string `yaml:"server"`
|
|
||||||
TLSServerName string `yaml:"tls-server-name,omitempty"`
|
|
||||||
InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify,omitempty"`
|
|
||||||
CertificateAuthority string `yaml:"certificate-authority,omitempty"`
|
|
||||||
CertificateAuthorityData string `yaml:"certificate-authority-data,omitempty"`
|
|
||||||
ProxyURL string `yaml:"proxy-url,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthInfo contains information that describes identity information. This is use to tell the kubernetes cluster who you are.
|
|
||||||
type AuthInfo struct {
|
|
||||||
LocationOfOrigin string
|
|
||||||
ClientCertificate string `yaml:"client-certificate,omitempty"`
|
|
||||||
ClientCertificateData string `yaml:"client-certificate-data,omitempty"`
|
|
||||||
ClientKey string `yaml:"client-key,omitempty"`
|
|
||||||
ClientKeyData string `yaml:"client-key-data,omitempty"`
|
|
||||||
Token string `yaml:"token,omitempty"`
|
|
||||||
TokenFile string `yaml:"tokenFile,omitempty"`
|
|
||||||
Impersonate string `yaml:"act-as,omitempty"`
|
|
||||||
ImpersonateUID string `yaml:"act-as-uid,omitempty"`
|
|
||||||
ImpersonateGroups []string `yaml:"act-as-groups,omitempty"`
|
|
||||||
ImpersonateUserExtra []string `yaml:"act-as-user-extra,omitempty"`
|
|
||||||
Username string `yaml:"username,omitempty"`
|
|
||||||
Password string `yaml:"password,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Context is a tuple of references to a cluster (how do I communicate with a kubernetes cluster), a user (how do I identify myself), and a namespace (what subset of resources do I want to work with)
|
|
||||||
type Context struct {
|
|
||||||
LocationOfOrigin string
|
|
||||||
Cluster string `yaml:"cluster"`
|
|
||||||
AuthInfo string `yaml:"user"`
|
|
||||||
Namespace string `yaml:"namespace,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ApiConfig struct {
|
|
||||||
basicAuth *promauth.BasicAuthConfig
|
|
||||||
server string
|
|
||||||
token string
|
|
||||||
tokenFile string
|
|
||||||
tlsConfig *promauth.TLSConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildConfig(sdc *SDConfig) (*ApiConfig, error) {
|
|
||||||
|
|
||||||
data, err := fs.ReadFileOrHTTP(sdc.KubeConfig)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot read kubeConfig from %q: %w", sdc.KubeConfig, err)
|
|
||||||
}
|
|
||||||
var config Config
|
|
||||||
if err = yaml.Unmarshal(data, &config); err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot parse %q: %w", sdc.KubeConfig, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var authInfos = make(map[string]*AuthInfo)
|
|
||||||
for _, obj := range config.AuthInfos {
|
|
||||||
authInfos[obj.Name] = obj.AuthInfo
|
|
||||||
}
|
|
||||||
var clusterInfos = make(map[string]*Cluster)
|
|
||||||
for _, obj := range config.Clusters {
|
|
||||||
clusterInfos[obj.Name] = obj.Cluster
|
|
||||||
}
|
|
||||||
var contexts = make(map[string]*Context)
|
|
||||||
for _, obj := range config.Contexts {
|
|
||||||
contexts[obj.Name] = obj.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
contextName := config.CurrentContext
|
|
||||||
configContext, exists := contexts[contextName]
|
|
||||||
if !exists {
|
|
||||||
return nil, fmt.Errorf("context %q does not exist", contextName)
|
|
||||||
}
|
|
||||||
|
|
||||||
clusterInfoName := configContext.Cluster
|
|
||||||
configClusterInfo, exists := clusterInfos[clusterInfoName]
|
|
||||||
if !exists {
|
|
||||||
return nil, fmt.Errorf("cluster %q does not exist", clusterInfoName)
|
|
||||||
}
|
|
||||||
|
|
||||||
authInfoName := configContext.AuthInfo
|
|
||||||
configAuthInfo, exists := authInfos[authInfoName]
|
|
||||||
if authInfoName != "" && !exists {
|
|
||||||
return nil, fmt.Errorf("auth info %q does not exist", authInfoName)
|
|
||||||
}
|
|
||||||
|
|
||||||
var apiConfig ApiConfig
|
|
||||||
|
|
||||||
apiConfig.tlsConfig = &promauth.TLSConfig{
|
|
||||||
CAFile: configClusterInfo.CertificateAuthority,
|
|
||||||
ServerName: configClusterInfo.TLSServerName,
|
|
||||||
InsecureSkipVerify: configClusterInfo.InsecureSkipTLSVerify,
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(configClusterInfo.CertificateAuthorityData) != 0 {
|
|
||||||
apiConfig.tlsConfig.CA, err = base64.StdEncoding.DecodeString(configClusterInfo.CertificateAuthorityData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot base64-decode configClusterInfo.CertificateAuthorityData %q: %w", configClusterInfo.CertificateAuthorityData, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if configAuthInfo != nil {
|
|
||||||
apiConfig.tlsConfig.CertFile = configAuthInfo.ClientCertificate
|
|
||||||
apiConfig.tlsConfig.KeyFile = configAuthInfo.ClientKey
|
|
||||||
apiConfig.token = configAuthInfo.Token
|
|
||||||
apiConfig.tokenFile = configAuthInfo.TokenFile
|
|
||||||
if len(configAuthInfo.ClientCertificateData) != 0 {
|
|
||||||
apiConfig.tlsConfig.Cert, err = base64.StdEncoding.DecodeString(configAuthInfo.ClientCertificateData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot base64-decode configAuthInfo.ClientCertificateData %q: %w", configClusterInfo.CertificateAuthorityData, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(configAuthInfo.ClientKeyData) != 0 {
|
|
||||||
apiConfig.tlsConfig.Key, err = base64.StdEncoding.DecodeString(configAuthInfo.ClientKeyData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot base64-decode configAuthInfo.ClientKeyData %q: %w", configClusterInfo.CertificateAuthorityData, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(configAuthInfo.Username) > 0 || len(configAuthInfo.Password) > 0 {
|
|
||||||
apiConfig.basicAuth = &promauth.BasicAuthConfig{
|
|
||||||
Username: configAuthInfo.Username,
|
|
||||||
Password: promauth.NewSecret(configAuthInfo.Password),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apiConfig.server = configClusterInfo.Server
|
|
||||||
|
|
||||||
return &apiConfig, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAPIConfig(sdc *SDConfig, baseDir string, swcFunc ScrapeWorkConstructorFunc) (*apiConfig, error) {
|
func newAPIConfig(sdc *SDConfig, baseDir string, swcFunc ScrapeWorkConstructorFunc) (*apiConfig, error) {
|
||||||
role := sdc.role()
|
role := sdc.role()
|
||||||
switch role {
|
switch role {
|
||||||
|
@ -183,17 +22,18 @@ func newAPIConfig(sdc *SDConfig, baseDir string, swcFunc ScrapeWorkConstructorFu
|
||||||
}
|
}
|
||||||
apiServer := sdc.APIServer
|
apiServer := sdc.APIServer
|
||||||
|
|
||||||
if len(sdc.KubeConfig) != 0 {
|
if len(sdc.KubeConfig) > 0 {
|
||||||
config, err := buildConfig(sdc)
|
fmt.Println("building")
|
||||||
|
kc, err := buildConfig(sdc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot parse kube config: %w", err)
|
return nil, fmt.Errorf("cannot build kube config: %w", err)
|
||||||
}
|
}
|
||||||
acNew, err := promauth.NewConfig(".", nil, config.basicAuth, config.token, config.tokenFile, nil, config.tlsConfig)
|
ac, err = promauth.NewConfig(".", nil, kc.basicAuth, kc.token, kc.tokenFile, nil, kc.tlsConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot initialize service account auth: %w; probably, `kubernetes_sd_config->api_server` is missing in Prometheus configs?", err)
|
return nil, fmt.Errorf("cannot initialize service account auth: %w; probably, `kubernetes_sd_config->api_server` is missing in Prometheus configs?", err)
|
||||||
}
|
}
|
||||||
ac = acNew
|
apiServer = kc.server
|
||||||
apiServer = config.server
|
sdc.ProxyURL = kc.proxyURL
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(apiServer) == 0 {
|
if len(apiServer) == 0 {
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
package kubernetes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseKubeConfig(t *testing.T) {
|
|
||||||
|
|
||||||
type testCase struct {
|
|
||||||
name string
|
|
||||||
sdc *SDConfig
|
|
||||||
expectedConfig *ApiConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
var cases = []testCase{
|
|
||||||
{
|
|
||||||
name: "token",
|
|
||||||
sdc: &SDConfig{
|
|
||||||
KubeConfig: "testdata/kubeconfig_token.yaml",
|
|
||||||
},
|
|
||||||
expectedConfig: &ApiConfig{
|
|
||||||
token: "abc",
|
|
||||||
tlsConfig: &promauth.TLSConfig{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "cert",
|
|
||||||
sdc: &SDConfig{
|
|
||||||
KubeConfig: "testdata/kubeconfig_cert.yaml",
|
|
||||||
},
|
|
||||||
expectedConfig: &ApiConfig{
|
|
||||||
server: "localhost:8000",
|
|
||||||
tlsConfig: &promauth.TLSConfig{
|
|
||||||
CA: []byte("authority"),
|
|
||||||
Cert: []byte("certificate"),
|
|
||||||
Key: []byte("key"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "basic",
|
|
||||||
sdc: &SDConfig{
|
|
||||||
KubeConfig: "testdata/kubeconfig_basic.yaml",
|
|
||||||
},
|
|
||||||
expectedConfig: &ApiConfig{
|
|
||||||
basicAuth: &promauth.BasicAuthConfig{
|
|
||||||
Password: promauth.NewSecret("secret"),
|
|
||||||
Username: "user1",
|
|
||||||
},
|
|
||||||
tlsConfig: &promauth.TLSConfig{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tc := range cases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
ac, err := buildConfig(tc.sdc)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(ac, tc.expectedConfig) {
|
|
||||||
t.Fatalf("unexpected result, got: %v, want: %v", ac, tc.expectedConfig)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
261
lib/promscrape/discovery/kubernetes/kubeconfig.go
Normal file
261
lib/promscrape/discovery/kubernetes/kubeconfig.go
Normal file
|
@ -0,0 +1,261 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/proxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// apiConfig contains config for API server
|
||||||
|
type apiConfig struct {
|
||||||
|
aw *apiWatcher
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config represent configuration file for kubernetes API server connection
|
||||||
|
// https://github.com/kubernetes/client-go/blob/master/tools/clientcmd/api/v1/types.go#L28
|
||||||
|
type Config struct {
|
||||||
|
Kind string `yaml:"kind,omitempty"`
|
||||||
|
APIVersion string `yaml:"apiVersion,omitempty"`
|
||||||
|
Clusters []struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Cluster *Cluster `yaml:"cluster"`
|
||||||
|
} `yaml:"clusters"`
|
||||||
|
AuthInfos []struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
AuthInfo *AuthInfo `yaml:"user"`
|
||||||
|
} `yaml:"users"`
|
||||||
|
Contexts []struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Context *Context `yaml:"context"`
|
||||||
|
} `yaml:"contexts"`
|
||||||
|
CurrentContext string `yaml:"current-context"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cluster contains information about how to communicate with a kubernetes cluster
|
||||||
|
type Cluster struct {
|
||||||
|
Server string `yaml:"server"`
|
||||||
|
TLSServerName string `yaml:"tls-server-name,omitempty"`
|
||||||
|
InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify,omitempty"`
|
||||||
|
CertificateAuthority string `yaml:"certificate-authority,omitempty"`
|
||||||
|
CertificateAuthorityData string `yaml:"certificate-authority-data,omitempty"`
|
||||||
|
ProxyURL *proxy.URL `yaml:"proxy-url,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthInfo contains information that describes identity information. This is use to tell the kubernetes cluster who you are.
|
||||||
|
type AuthInfo struct {
|
||||||
|
ClientCertificate string `yaml:"client-certificate,omitempty"`
|
||||||
|
ClientCertificateData string `yaml:"client-certificate-data,omitempty"`
|
||||||
|
ClientKey string `yaml:"client-key,omitempty"`
|
||||||
|
ClientKeyData string `yaml:"client-key-data,omitempty"`
|
||||||
|
// TODO add support for it
|
||||||
|
Exec *ExecConfig `yaml:"exec,omitempty"`
|
||||||
|
Token string `yaml:"token,omitempty"`
|
||||||
|
TokenFile string `yaml:"tokenFile,omitempty"`
|
||||||
|
Impersonate string `yaml:"act-as,omitempty"`
|
||||||
|
ImpersonateUID string `yaml:"act-as-uid,omitempty"`
|
||||||
|
ImpersonateGroups []string `yaml:"act-as-groups,omitempty"`
|
||||||
|
ImpersonateUserExtra []string `yaml:"act-as-user-extra,omitempty"`
|
||||||
|
Username string `yaml:"username,omitempty"`
|
||||||
|
Password string `yaml:"password,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (au *AuthInfo) validate() error {
|
||||||
|
errContext := "field: %s is not supported currently, open an issue with feature request for it"
|
||||||
|
if au.Exec != nil {
|
||||||
|
return fmt.Errorf(errContext, "exec")
|
||||||
|
}
|
||||||
|
if len(au.ImpersonateUID) > 0 {
|
||||||
|
return fmt.Errorf(errContext, "act-as-uid")
|
||||||
|
}
|
||||||
|
if len(au.Impersonate) > 0 {
|
||||||
|
return fmt.Errorf(errContext, "act-as")
|
||||||
|
}
|
||||||
|
if len(au.ImpersonateGroups) > 0 {
|
||||||
|
return fmt.Errorf(errContext, "act-as-groups")
|
||||||
|
}
|
||||||
|
if len(au.ImpersonateUserExtra) > 0 {
|
||||||
|
return fmt.Errorf(errContext, "act-as-user-extra")
|
||||||
|
}
|
||||||
|
if len(au.Password) > 0 && len(au.Username) == 0 {
|
||||||
|
return fmt.Errorf("username cannot be empty, if password defined")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecConfig contains information about os.command, that returns auth token for kubernetes cluster connection
|
||||||
|
type ExecConfig struct {
|
||||||
|
// Command to execute.
|
||||||
|
Command string `json:"command"`
|
||||||
|
// Arguments to pass to the command when executing it.
|
||||||
|
Args []string `json:"args"`
|
||||||
|
// Env defines additional environment variables to expose to the process. These
|
||||||
|
// are unioned with the host's environment, as well as variables client-go uses
|
||||||
|
// to pass argument to the plugin.
|
||||||
|
Env []ExecEnvVar `json:"env"`
|
||||||
|
|
||||||
|
// Preferred input version of the ExecInfo. The returned ExecCredentials MUST use
|
||||||
|
// the same encoding version as the input.
|
||||||
|
APIVersion string `json:"apiVersion,omitempty"`
|
||||||
|
|
||||||
|
// This text is shown to the user when the executable doesn't seem to be
|
||||||
|
// present. For example, `brew install foo-cli` might be a good InstallHint for
|
||||||
|
// foo-cli on Mac OS systems.
|
||||||
|
InstallHint string `json:"installHint,omitempty"`
|
||||||
|
|
||||||
|
// ProvideClusterInfo determines whether or not to provide cluster information,
|
||||||
|
// which could potentially contain very large CA data, to this exec plugin as a
|
||||||
|
// part of the KUBERNETES_EXEC_INFO environment variable. By default, it is set
|
||||||
|
// to false. Package k8s.io/client-go/tools/auth/exec provides helper methods for
|
||||||
|
// reading this environment variable.
|
||||||
|
ProvideClusterInfo bool `json:"provideClusterInfo"`
|
||||||
|
|
||||||
|
// InteractiveMode determines this plugin's relationship with standard input. Valid
|
||||||
|
// values are "Never" (this exec plugin never uses standard input), "IfAvailable" (this
|
||||||
|
// exec plugin wants to use standard input if it is available), or "Always" (this exec
|
||||||
|
// plugin requires standard input to function). See ExecInteractiveMode values for more
|
||||||
|
// details.
|
||||||
|
//
|
||||||
|
// If APIVersion is client.authentication.k8s.io/v1alpha1 or
|
||||||
|
// client.authentication.k8s.io/v1beta1, then this field is optional and defaults
|
||||||
|
// to "IfAvailable" when unset. Otherwise, this field is required.
|
||||||
|
//+optional
|
||||||
|
InteractiveMode string `json:"interactiveMode,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecEnvVar is used for setting environment variables when executing an exec-based
|
||||||
|
// credential plugin.
|
||||||
|
type ExecEnvVar struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context is a tuple of references to a cluster and AuthInfo
|
||||||
|
type Context struct {
|
||||||
|
Cluster string `yaml:"cluster"`
|
||||||
|
AuthInfo string `yaml:"user"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type kubeConfig struct {
|
||||||
|
basicAuth *promauth.BasicAuthConfig
|
||||||
|
server string
|
||||||
|
token string
|
||||||
|
tokenFile string
|
||||||
|
tlsConfig *promauth.TLSConfig
|
||||||
|
proxyURL *proxy.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildConfig(sdc *SDConfig) (*kubeConfig, error) {
|
||||||
|
|
||||||
|
data, err := fs.ReadFileOrHTTP(sdc.KubeConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot read kubeConfig from %q: %w", sdc.KubeConfig, err)
|
||||||
|
}
|
||||||
|
var config Config
|
||||||
|
if err = yaml.Unmarshal(data, &config); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot parse %q: %w", sdc.KubeConfig, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
authInfos := make(map[string]*AuthInfo)
|
||||||
|
for _, obj := range config.AuthInfos {
|
||||||
|
authInfos[obj.Name] = obj.AuthInfo
|
||||||
|
}
|
||||||
|
clusterInfos := make(map[string]*Cluster)
|
||||||
|
for _, obj := range config.Clusters {
|
||||||
|
clusterInfos[obj.Name] = obj.Cluster
|
||||||
|
}
|
||||||
|
contexts := make(map[string]*Context)
|
||||||
|
for _, obj := range config.Contexts {
|
||||||
|
contexts[obj.Name] = obj.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
contextName := config.CurrentContext
|
||||||
|
configContext := contexts[contextName]
|
||||||
|
if configContext == nil {
|
||||||
|
return nil, fmt.Errorf("context %q does not exist", contextName)
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterInfoName := configContext.Cluster
|
||||||
|
configClusterInfo := clusterInfos[clusterInfoName]
|
||||||
|
if configClusterInfo == nil {
|
||||||
|
return nil, fmt.Errorf("cluster %q does not exist", clusterInfoName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(configClusterInfo.Server) == 0 {
|
||||||
|
return nil, fmt.Errorf("kubernetes server address cannot be empty, define it for context: %s", contextName)
|
||||||
|
}
|
||||||
|
|
||||||
|
authInfoName := configContext.AuthInfo
|
||||||
|
configAuthInfo := authInfos[authInfoName]
|
||||||
|
if authInfoName != "" && configAuthInfo == nil {
|
||||||
|
return nil, fmt.Errorf("auth info %q does not exist", authInfoName)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tlsConfig *promauth.TLSConfig
|
||||||
|
var basicAuth *promauth.BasicAuthConfig
|
||||||
|
var token, tokenFile string
|
||||||
|
isHTTPS := strings.HasPrefix(configClusterInfo.Server, "https://")
|
||||||
|
|
||||||
|
if isHTTPS {
|
||||||
|
tlsConfig = &promauth.TLSConfig{
|
||||||
|
CAFile: configClusterInfo.CertificateAuthority,
|
||||||
|
ServerName: configClusterInfo.TLSServerName,
|
||||||
|
InsecureSkipVerify: configClusterInfo.InsecureSkipTLSVerify,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(configClusterInfo.CertificateAuthorityData) > 0 && isHTTPS {
|
||||||
|
tlsConfig.CA, err = base64.StdEncoding.DecodeString(configClusterInfo.CertificateAuthorityData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot base64-decode configClusterInfo.CertificateAuthorityData %q: %w", configClusterInfo.CertificateAuthorityData, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if configAuthInfo != nil {
|
||||||
|
if err := configAuthInfo.validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid user auth configuration for context: %s, err: %w", contextName, err)
|
||||||
|
}
|
||||||
|
if isHTTPS {
|
||||||
|
tlsConfig.CertFile = configAuthInfo.ClientCertificate
|
||||||
|
tlsConfig.KeyFile = configAuthInfo.ClientKey
|
||||||
|
|
||||||
|
if len(configAuthInfo.ClientCertificateData) > 0 {
|
||||||
|
tlsConfig.Cert, err = base64.StdEncoding.DecodeString(configAuthInfo.ClientCertificateData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot base64-decode configAuthInfo.ClientCertificateData %q: %w", configClusterInfo.CertificateAuthorityData, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(configAuthInfo.ClientKeyData) > 0 {
|
||||||
|
tlsConfig.Key, err = base64.StdEncoding.DecodeString(configAuthInfo.ClientKeyData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot base64-decode configAuthInfo.ClientKeyData %q: %w", configClusterInfo.CertificateAuthorityData, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(configAuthInfo.Username) > 0 || len(configAuthInfo.Password) > 0 {
|
||||||
|
basicAuth = &promauth.BasicAuthConfig{
|
||||||
|
Username: configAuthInfo.Username,
|
||||||
|
Password: promauth.NewSecret(configAuthInfo.Password),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
token = configAuthInfo.Token
|
||||||
|
tokenFile = configAuthInfo.TokenFile
|
||||||
|
}
|
||||||
|
|
||||||
|
kc := kubeConfig{
|
||||||
|
basicAuth: basicAuth,
|
||||||
|
server: configClusterInfo.Server,
|
||||||
|
token: token,
|
||||||
|
tokenFile: tokenFile,
|
||||||
|
tlsConfig: tlsConfig,
|
||||||
|
proxyURL: configClusterInfo.ProxyURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &kc, nil
|
||||||
|
}
|
84
lib/promscrape/discovery/kubernetes/kubeconfig_test.go
Normal file
84
lib/promscrape/discovery/kubernetes/kubeconfig_test.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseKubeConfigSuccess(t *testing.T) {
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
sdc *SDConfig
|
||||||
|
expectedConfig *kubeConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
var cases = []testCase{
|
||||||
|
{
|
||||||
|
name: "token",
|
||||||
|
sdc: &SDConfig{
|
||||||
|
KubeConfig: "testdata/good_kubeconfig/with_token.yaml",
|
||||||
|
},
|
||||||
|
expectedConfig: &kubeConfig{
|
||||||
|
server: "http://some-server:8080",
|
||||||
|
token: "abc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cert",
|
||||||
|
sdc: &SDConfig{
|
||||||
|
KubeConfig: "testdata/good_kubeconfig/with_tls.yaml",
|
||||||
|
},
|
||||||
|
expectedConfig: &kubeConfig{
|
||||||
|
server: "https://localhost:6443",
|
||||||
|
tlsConfig: &promauth.TLSConfig{
|
||||||
|
CA: []byte("authority"),
|
||||||
|
Cert: []byte("certificate"),
|
||||||
|
Key: []byte("key"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "basic",
|
||||||
|
sdc: &SDConfig{
|
||||||
|
KubeConfig: "testdata/good_kubeconfig/with_basic.yaml",
|
||||||
|
},
|
||||||
|
expectedConfig: &kubeConfig{
|
||||||
|
server: "http://some-server:8080",
|
||||||
|
basicAuth: &promauth.BasicAuthConfig{
|
||||||
|
Password: promauth.NewSecret("secret"),
|
||||||
|
Username: "user1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
ac, err := buildConfig(tc.sdc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(ac, tc.expectedConfig) {
|
||||||
|
t.Fatalf("unexpected result, got: %v, want: %v", ac, tc.expectedConfig)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseKubeConfigFail(t *testing.T) {
|
||||||
|
f := func(name, kubeConfigPath string) {
|
||||||
|
t.Helper()
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
sdc := &SDConfig{
|
||||||
|
KubeConfig: kubeConfigPath,
|
||||||
|
}
|
||||||
|
if _, err := buildConfig(sdc); err == nil {
|
||||||
|
t.Fatalf("unexpected result for config file: %s, must return error", kubeConfigPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
f("unsupported options", "testdata/bad_kubeconfig/unsupported_fields")
|
||||||
|
f("missing server address", "testdata/bad_kubeconfig/missing_server.yaml")
|
||||||
|
}
|
|
@ -21,7 +21,8 @@ type SDConfig struct {
|
||||||
APIServer string `yaml:"api_server,omitempty"`
|
APIServer string `yaml:"api_server,omitempty"`
|
||||||
|
|
||||||
// Use role() function for accessing the Role field
|
// Use role() function for accessing the Role field
|
||||||
Role string `yaml:"role"`
|
Role string `yaml:"role"`
|
||||||
|
// if defined any cluster connection information from HTTPClientConfig will be ignored
|
||||||
KubeConfig string `yaml:"kubeconfig_file"`
|
KubeConfig string `yaml:"kubeconfig_file"`
|
||||||
|
|
||||||
HTTPClientConfig promauth.HTTPClientConfig `yaml:",inline"`
|
HTTPClientConfig promauth.HTTPClientConfig `yaml:",inline"`
|
||||||
|
|
11
lib/promscrape/discovery/kubernetes/testdata/bad_kubeconfig/missing_server.yaml
vendored
Normal file
11
lib/promscrape/discovery/kubernetes/testdata/bad_kubeconfig/missing_server.yaml
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
apiVersion: v1
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
name: k8s
|
||||||
|
contexts:
|
||||||
|
- context:
|
||||||
|
cluster: k8s
|
||||||
|
name: user1@k8s
|
||||||
|
current-context: user1@k8s
|
||||||
|
kind: Config
|
||||||
|
preferences: {}
|
30
lib/promscrape/discovery/kubernetes/testdata/bad_kubeconfig/unsupported_fields.yaml
vendored
Normal file
30
lib/promscrape/discovery/kubernetes/testdata/bad_kubeconfig/unsupported_fields.yaml
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
apiVersion: v1
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
server: "http://some-server:8080"
|
||||||
|
name: k8s
|
||||||
|
contexts:
|
||||||
|
- context:
|
||||||
|
cluster: k8s
|
||||||
|
user: user1
|
||||||
|
name: user1@k8s
|
||||||
|
current-context: user1@k8s
|
||||||
|
kind: Config
|
||||||
|
preferences: {}
|
||||||
|
users:
|
||||||
|
- name: user1
|
||||||
|
exec:
|
||||||
|
apiVersion: client.authentication.k8s.io/v1alpha1
|
||||||
|
args:
|
||||||
|
- eks
|
||||||
|
- get-token
|
||||||
|
- --cluster-name
|
||||||
|
- some-cluster
|
||||||
|
- --region
|
||||||
|
- us-east-2
|
||||||
|
command: aws
|
||||||
|
env:
|
||||||
|
- name: AWS_STS_REGIONAL_ENDPOINTS
|
||||||
|
value: regional
|
||||||
|
interactiveMode: IfAvailable
|
||||||
|
provideClusterInfo: false
|
|
@ -1,7 +1,7 @@
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
clusters:
|
clusters:
|
||||||
- cluster:
|
- cluster:
|
||||||
server: ""
|
server: "http://some-server:8080"
|
||||||
name: k8s
|
name: k8s
|
||||||
contexts:
|
contexts:
|
||||||
- context:
|
- context:
|
|
@ -2,7 +2,7 @@ apiVersion: v1
|
||||||
clusters:
|
clusters:
|
||||||
- cluster:
|
- cluster:
|
||||||
certificate-authority-data: YXV0aG9yaXR5
|
certificate-authority-data: YXV0aG9yaXR5
|
||||||
server: localhost:8000
|
server: https://localhost:6443
|
||||||
name: k8s
|
name: k8s
|
||||||
contexts:
|
contexts:
|
||||||
- context:
|
- context:
|
|
@ -1,7 +1,7 @@
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
clusters:
|
clusters:
|
||||||
- cluster:
|
- cluster:
|
||||||
server: ""
|
server: "http://some-server:8080"
|
||||||
name: k8s
|
name: k8s
|
||||||
contexts:
|
contexts:
|
||||||
- context:
|
- context:
|
Loading…
Reference in a new issue