From a18914abeed1e6a80727fff99fca32d2d87e38e8 Mon Sep 17 00:00:00 2001 From: Nikolay Date: Wed, 1 Jun 2022 20:44:45 +0200 Subject: [PATCH] lib/promscrape/discovery/kubernetes: follow-up after 0b5c8749117299645c190840e10712a76f525ed4 (#2672) --- docs/CHANGELOG.md | 1 + lib/promauth/config.go | 11 +- lib/promscrape/discovery/kubernetes/api.go | 174 +----------- .../discovery/kubernetes/api_test.go | 68 ----- .../discovery/kubernetes/kubeconfig.go | 261 ++++++++++++++++++ .../discovery/kubernetes/kubeconfig_test.go | 84 ++++++ .../discovery/kubernetes/kubernetes.go | 3 +- .../bad_kubeconfig/missing_server.yaml | 11 + .../bad_kubeconfig/unsupported_fields.yaml | 30 ++ .../with_basic.yaml} | 2 +- .../with_tls.yaml} | 2 +- .../with_token.yaml} | 2 +- 12 files changed, 404 insertions(+), 245 deletions(-) delete mode 100644 lib/promscrape/discovery/kubernetes/api_test.go create mode 100644 lib/promscrape/discovery/kubernetes/kubeconfig.go create mode 100644 lib/promscrape/discovery/kubernetes/kubeconfig_test.go create mode 100644 lib/promscrape/discovery/kubernetes/testdata/bad_kubeconfig/missing_server.yaml create mode 100644 lib/promscrape/discovery/kubernetes/testdata/bad_kubeconfig/unsupported_fields.yaml rename lib/promscrape/discovery/kubernetes/testdata/{kubeconfig_basic.yaml => good_kubeconfig/with_basic.yaml} (79%) rename lib/promscrape/discovery/kubernetes/testdata/{kubeconfig_cert.yaml => good_kubeconfig/with_tls.yaml} (83%) rename lib/promscrape/discovery/kubernetes/testdata/{kubeconfig_token.yaml => good_kubeconfig/with_token.yaml} (79%) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 21ab0626c..a66a2b289 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -15,6 +15,7 @@ The following tip changes can be tested by building VictoriaMetrics components f ## 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: 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). diff --git a/lib/promauth/config.go b/lib/promauth/config.go index e96f01bb6..bb54ef7d6 100644 --- a/lib/promauth/config.go +++ b/lib/promauth/config.go @@ -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 &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. if _, err := getTLSCert(nil); err != nil { diff --git a/lib/promscrape/discovery/kubernetes/api.go b/lib/promscrape/discovery/kubernetes/api.go index 906470338..36ffa67be 100644 --- a/lib/promscrape/discovery/kubernetes/api.go +++ b/lib/promscrape/discovery/kubernetes/api.go @@ -1,10 +1,7 @@ package kubernetes import ( - "encoding/base64" "fmt" - "github.com/VictoriaMetrics/VictoriaMetrics/lib/fs" - "gopkg.in/yaml.v2" "net" "os" "strings" @@ -12,164 +9,6 @@ import ( "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) { role := sdc.role() switch role { @@ -183,17 +22,18 @@ func newAPIConfig(sdc *SDConfig, baseDir string, swcFunc ScrapeWorkConstructorFu } apiServer := sdc.APIServer - if len(sdc.KubeConfig) != 0 { - config, err := buildConfig(sdc) + if len(sdc.KubeConfig) > 0 { + fmt.Println("building") + kc, err := buildConfig(sdc) 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 { 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 = config.server + apiServer = kc.server + sdc.ProxyURL = kc.proxyURL } if len(apiServer) == 0 { diff --git a/lib/promscrape/discovery/kubernetes/api_test.go b/lib/promscrape/discovery/kubernetes/api_test.go deleted file mode 100644 index a012093c2..000000000 --- a/lib/promscrape/discovery/kubernetes/api_test.go +++ /dev/null @@ -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) - } - }) - } - -} diff --git a/lib/promscrape/discovery/kubernetes/kubeconfig.go b/lib/promscrape/discovery/kubernetes/kubeconfig.go new file mode 100644 index 000000000..0971640fa --- /dev/null +++ b/lib/promscrape/discovery/kubernetes/kubeconfig.go @@ -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 +} diff --git a/lib/promscrape/discovery/kubernetes/kubeconfig_test.go b/lib/promscrape/discovery/kubernetes/kubeconfig_test.go new file mode 100644 index 000000000..cb50ff94c --- /dev/null +++ b/lib/promscrape/discovery/kubernetes/kubeconfig_test.go @@ -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") +} diff --git a/lib/promscrape/discovery/kubernetes/kubernetes.go b/lib/promscrape/discovery/kubernetes/kubernetes.go index edd69b860..d5ed48f3a 100644 --- a/lib/promscrape/discovery/kubernetes/kubernetes.go +++ b/lib/promscrape/discovery/kubernetes/kubernetes.go @@ -21,7 +21,8 @@ type SDConfig struct { APIServer string `yaml:"api_server,omitempty"` // 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"` HTTPClientConfig promauth.HTTPClientConfig `yaml:",inline"` diff --git a/lib/promscrape/discovery/kubernetes/testdata/bad_kubeconfig/missing_server.yaml b/lib/promscrape/discovery/kubernetes/testdata/bad_kubeconfig/missing_server.yaml new file mode 100644 index 000000000..ad8da1866 --- /dev/null +++ b/lib/promscrape/discovery/kubernetes/testdata/bad_kubeconfig/missing_server.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +clusters: + - cluster: + name: k8s +contexts: + - context: + cluster: k8s + name: user1@k8s +current-context: user1@k8s +kind: Config +preferences: {} \ No newline at end of file diff --git a/lib/promscrape/discovery/kubernetes/testdata/bad_kubeconfig/unsupported_fields.yaml b/lib/promscrape/discovery/kubernetes/testdata/bad_kubeconfig/unsupported_fields.yaml new file mode 100644 index 000000000..e875e374e --- /dev/null +++ b/lib/promscrape/discovery/kubernetes/testdata/bad_kubeconfig/unsupported_fields.yaml @@ -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 \ No newline at end of file diff --git a/lib/promscrape/discovery/kubernetes/testdata/kubeconfig_basic.yaml b/lib/promscrape/discovery/kubernetes/testdata/good_kubeconfig/with_basic.yaml similarity index 79% rename from lib/promscrape/discovery/kubernetes/testdata/kubeconfig_basic.yaml rename to lib/promscrape/discovery/kubernetes/testdata/good_kubeconfig/with_basic.yaml index a3fa85de3..c666a0905 100644 --- a/lib/promscrape/discovery/kubernetes/testdata/kubeconfig_basic.yaml +++ b/lib/promscrape/discovery/kubernetes/testdata/good_kubeconfig/with_basic.yaml @@ -1,7 +1,7 @@ apiVersion: v1 clusters: - cluster: - server: "" + server: "http://some-server:8080" name: k8s contexts: - context: diff --git a/lib/promscrape/discovery/kubernetes/testdata/kubeconfig_cert.yaml b/lib/promscrape/discovery/kubernetes/testdata/good_kubeconfig/with_tls.yaml similarity index 83% rename from lib/promscrape/discovery/kubernetes/testdata/kubeconfig_cert.yaml rename to lib/promscrape/discovery/kubernetes/testdata/good_kubeconfig/with_tls.yaml index e14136938..df0c86c69 100644 --- a/lib/promscrape/discovery/kubernetes/testdata/kubeconfig_cert.yaml +++ b/lib/promscrape/discovery/kubernetes/testdata/good_kubeconfig/with_tls.yaml @@ -2,7 +2,7 @@ apiVersion: v1 clusters: - cluster: certificate-authority-data: YXV0aG9yaXR5 - server: localhost:8000 + server: https://localhost:6443 name: k8s contexts: - context: diff --git a/lib/promscrape/discovery/kubernetes/testdata/kubeconfig_token.yaml b/lib/promscrape/discovery/kubernetes/testdata/good_kubeconfig/with_token.yaml similarity index 79% rename from lib/promscrape/discovery/kubernetes/testdata/kubeconfig_token.yaml rename to lib/promscrape/discovery/kubernetes/testdata/good_kubeconfig/with_token.yaml index 6c715ba38..e79af6900 100644 --- a/lib/promscrape/discovery/kubernetes/testdata/kubeconfig_token.yaml +++ b/lib/promscrape/discovery/kubernetes/testdata/good_kubeconfig/with_token.yaml @@ -1,7 +1,7 @@ apiVersion: v1 clusters: - cluster: - server: "" + server: "http://some-server:8080" name: k8s contexts: - context: