2022-06-01 18:44:45 +00:00
|
|
|
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 {
|
|
|
|
if au.Exec != nil {
|
2022-06-06 22:21:53 +00:00
|
|
|
return unsupportedFieldError("exec")
|
2022-06-01 18:44:45 +00:00
|
|
|
}
|
|
|
|
if len(au.ImpersonateUID) > 0 {
|
2022-06-06 22:21:53 +00:00
|
|
|
return unsupportedFieldError("act-as-uid")
|
2022-06-01 18:44:45 +00:00
|
|
|
}
|
|
|
|
if len(au.Impersonate) > 0 {
|
2022-06-06 22:21:53 +00:00
|
|
|
return unsupportedFieldError("act-as")
|
2022-06-01 18:44:45 +00:00
|
|
|
}
|
|
|
|
if len(au.ImpersonateGroups) > 0 {
|
2022-06-06 22:21:53 +00:00
|
|
|
return unsupportedFieldError("act-as-groups")
|
2022-06-01 18:44:45 +00:00
|
|
|
}
|
|
|
|
if len(au.ImpersonateUserExtra) > 0 {
|
2022-06-06 22:21:53 +00:00
|
|
|
return unsupportedFieldError("act-as-user-extra")
|
2022-06-01 18:44:45 +00:00
|
|
|
}
|
|
|
|
if len(au.Password) > 0 && len(au.Username) == 0 {
|
|
|
|
return fmt.Errorf("username cannot be empty, if password defined")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-06-06 22:21:53 +00:00
|
|
|
func unsupportedFieldError(fieldName string) error {
|
|
|
|
return fmt.Errorf("field %q is not supported yet; if you feel it is needed please open a feature request "+
|
|
|
|
"at https://github.com/VictoriaMetrics/VictoriaMetrics/issues/new", fieldName)
|
|
|
|
}
|
|
|
|
|
2022-06-01 18:44:45 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2022-06-06 11:24:52 +00:00
|
|
|
func newKubeConfig(kubeConfigFile string) (*kubeConfig, error) {
|
|
|
|
data, err := fs.ReadFileOrHTTP(kubeConfigFile)
|
2022-06-01 18:44:45 +00:00
|
|
|
if err != nil {
|
2022-06-06 11:24:52 +00:00
|
|
|
return nil, fmt.Errorf("cannot read %q: %w", kubeConfigFile, err)
|
|
|
|
}
|
|
|
|
var cfg Config
|
|
|
|
if err = yaml.Unmarshal(data, &cfg); err != nil {
|
|
|
|
return nil, fmt.Errorf("cannot parse %q: %w", kubeConfigFile, err)
|
2022-06-01 18:44:45 +00:00
|
|
|
}
|
2022-06-06 11:24:52 +00:00
|
|
|
kc, err := cfg.buildKubeConfig()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("cannot build kubeConfig from %q: %w", kubeConfigFile, err)
|
2022-06-01 18:44:45 +00:00
|
|
|
}
|
2022-06-06 11:24:52 +00:00
|
|
|
return kc, nil
|
|
|
|
}
|
2022-06-01 18:44:45 +00:00
|
|
|
|
2022-06-06 11:24:52 +00:00
|
|
|
func (cfg *Config) buildKubeConfig() (*kubeConfig, error) {
|
2022-06-01 18:44:45 +00:00
|
|
|
authInfos := make(map[string]*AuthInfo)
|
2022-06-06 11:24:52 +00:00
|
|
|
for _, obj := range cfg.AuthInfos {
|
2022-06-01 18:44:45 +00:00
|
|
|
authInfos[obj.Name] = obj.AuthInfo
|
|
|
|
}
|
|
|
|
clusterInfos := make(map[string]*Cluster)
|
2022-06-06 11:24:52 +00:00
|
|
|
for _, obj := range cfg.Clusters {
|
2022-06-01 18:44:45 +00:00
|
|
|
clusterInfos[obj.Name] = obj.Cluster
|
|
|
|
}
|
|
|
|
contexts := make(map[string]*Context)
|
2022-06-06 11:24:52 +00:00
|
|
|
for _, obj := range cfg.Contexts {
|
2022-06-01 18:44:45 +00:00
|
|
|
contexts[obj.Name] = obj.Context
|
|
|
|
}
|
|
|
|
|
2022-06-06 11:24:52 +00:00
|
|
|
contextName := cfg.CurrentContext
|
2022-06-01 18:44:45 +00:00
|
|
|
configContext := contexts[contextName]
|
|
|
|
if configContext == nil {
|
2022-06-06 11:24:52 +00:00
|
|
|
return nil, fmt.Errorf("missing context %q", contextName)
|
2022-06-01 18:44:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
clusterInfoName := configContext.Cluster
|
|
|
|
configClusterInfo := clusterInfos[clusterInfoName]
|
|
|
|
if configClusterInfo == nil {
|
2022-06-06 11:24:52 +00:00
|
|
|
return nil, fmt.Errorf("missing cluster config %q at context %q", clusterInfoName, contextName)
|
2022-06-01 18:44:45 +00:00
|
|
|
}
|
2022-06-06 11:24:52 +00:00
|
|
|
server := configClusterInfo.Server
|
|
|
|
if len(server) == 0 {
|
|
|
|
return nil, fmt.Errorf("missing kubernetes server address for config %q at context %q", clusterInfoName, contextName)
|
2022-06-01 18:44:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
authInfoName := configContext.AuthInfo
|
|
|
|
configAuthInfo := authInfos[authInfoName]
|
|
|
|
if authInfoName != "" && configAuthInfo == nil {
|
2022-06-06 11:24:52 +00:00
|
|
|
return nil, fmt.Errorf("missing auth config %q", authInfoName)
|
2022-06-01 18:44:45 +00:00
|
|
|
}
|
|
|
|
var tlsConfig *promauth.TLSConfig
|
|
|
|
var basicAuth *promauth.BasicAuthConfig
|
|
|
|
var token, tokenFile string
|
|
|
|
if configAuthInfo != nil {
|
|
|
|
if err := configAuthInfo.validate(); err != nil {
|
2022-06-06 11:24:52 +00:00
|
|
|
return nil, fmt.Errorf("invalid auth config %q: %w", authInfoName, err)
|
2022-06-01 18:44:45 +00:00
|
|
|
}
|
2022-06-06 11:24:52 +00:00
|
|
|
if strings.HasPrefix(configClusterInfo.Server, "https://") {
|
|
|
|
tlsConfig = &promauth.TLSConfig{
|
|
|
|
CAFile: configClusterInfo.CertificateAuthority,
|
|
|
|
ServerName: configClusterInfo.TLSServerName,
|
|
|
|
InsecureSkipVerify: configClusterInfo.InsecureSkipTLSVerify,
|
|
|
|
}
|
|
|
|
if len(configClusterInfo.CertificateAuthorityData) > 0 {
|
|
|
|
ca, err := base64.StdEncoding.DecodeString(configClusterInfo.CertificateAuthorityData)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("cannot base64-decode certificate-authority-data from config %q at context %q: %w", clusterInfoName, contextName, err)
|
|
|
|
}
|
2023-10-25 21:12:19 +00:00
|
|
|
tlsConfig.CA = string(ca)
|
2022-06-06 11:24:52 +00:00
|
|
|
}
|
2022-06-01 18:44:45 +00:00
|
|
|
tlsConfig.CertFile = configAuthInfo.ClientCertificate
|
|
|
|
tlsConfig.KeyFile = configAuthInfo.ClientKey
|
|
|
|
|
|
|
|
if len(configAuthInfo.ClientCertificateData) > 0 {
|
2022-06-06 11:24:52 +00:00
|
|
|
cert, err := base64.StdEncoding.DecodeString(configAuthInfo.ClientCertificateData)
|
2022-06-01 18:44:45 +00:00
|
|
|
if err != nil {
|
2022-06-06 11:24:52 +00:00
|
|
|
return nil, fmt.Errorf("cannot base64-decode client-certificate-data from %q: %w", authInfoName, err)
|
2022-06-01 18:44:45 +00:00
|
|
|
}
|
2023-10-25 21:12:19 +00:00
|
|
|
tlsConfig.Cert = string(cert)
|
2022-06-01 18:44:45 +00:00
|
|
|
}
|
|
|
|
if len(configAuthInfo.ClientKeyData) > 0 {
|
2022-06-06 11:24:52 +00:00
|
|
|
key, err := base64.StdEncoding.DecodeString(configAuthInfo.ClientKeyData)
|
2022-06-01 18:44:45 +00:00
|
|
|
if err != nil {
|
2022-06-06 11:24:52 +00:00
|
|
|
return nil, fmt.Errorf("cannot base64-decode client-key-data from %q: %w", authInfoName, err)
|
2022-06-01 18:44:45 +00:00
|
|
|
}
|
2023-10-25 21:12:19 +00:00
|
|
|
tlsConfig.Key = string(key)
|
2022-06-01 18:44:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
2022-06-06 11:24:52 +00:00
|
|
|
kc := &kubeConfig{
|
2022-06-01 18:44:45 +00:00
|
|
|
basicAuth: basicAuth,
|
2022-06-06 11:24:52 +00:00
|
|
|
server: server,
|
2022-06-01 18:44:45 +00:00
|
|
|
token: token,
|
|
|
|
tokenFile: tokenFile,
|
|
|
|
tlsConfig: tlsConfig,
|
|
|
|
proxyURL: configClusterInfo.ProxyURL,
|
|
|
|
}
|
2022-06-06 11:24:52 +00:00
|
|
|
return kc, nil
|
2022-06-01 18:44:45 +00:00
|
|
|
}
|