diff --git a/pkg/multiclusterclient/cluster.go b/pkg/multiclusterclient/cluster.go deleted file mode 100644 index b2813d3..0000000 --- a/pkg/multiclusterclient/cluster.go +++ /dev/null @@ -1,131 +0,0 @@ -package multiclusterclient - -import ( - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd/api" -) - -// Constants from https://github.com/argoproj/argo-cd/blob/v2.8.4/pkg/apis/application/v1alpha1/cluster_constants.go#L45. -// k8sClientConfigQPS controls the QPS to be used in K8s REST client configs. -const k8sClientConfigQPS = 50 - -// Cluster is the definition of a cluster resource. -// It is based on the argocd Cluster struct because we use the same secret for external cluster authentication. -// See: https://github.com/argoproj/argo-cd/blob/v2.8.4/pkg/apis/application/v1alpha1/types.go#L1666. -type Cluster struct { - // Server is the API server URL of the Kubernetes cluster - Server string `json:"server" protobuf:"bytes,1,opt,name=server"` - // Name of the cluster. If omitted, will use the server address - Name string `json:"name" protobuf:"bytes,2,opt,name=name"` - // Config holds cluster information for connecting to a cluster - Config ClusterConfig `json:"config" protobuf:"bytes,3,opt,name=config"` -} - -// ClusterConfig is the configuration attributes. This structure is subset of the go-client -// rest.Config with annotations added for marshalling. -type ClusterConfig struct { - // Server requires Basic authentication - Username string `json:"username,omitempty" protobuf:"bytes,1,opt,name=username"` - Password string `json:"password,omitempty" protobuf:"bytes,2,opt,name=password"` - - // Server requires Bearer authentication. This client will not attempt to use - // refresh tokens for an OAuth2 flow. - BearerToken string `json:"bearerToken,omitempty" protobuf:"bytes,3,opt,name=bearerToken"` - - // TLSClientConfig contains settings to enable transport layer security - TLSClientConfig `json:"tlsClientConfig" protobuf:"bytes,4,opt,name=tlsClientConfig"` - - // ExecProviderConfig contains configuration for an exec provider - ExecProviderConfig *ExecProviderConfig `json:"execProviderConfig,omitempty" protobuf:"bytes,6,opt,name=execProviderConfig"` -} - -// TLSClientConfig contains settings to enable transport layer security. -type TLSClientConfig struct { - // Insecure specifies that the server should be accessed without verifying the TLS certificate. For testing only. - Insecure bool `json:"insecure" protobuf:"bytes,1,opt,name=insecure"` - // ServerName is passed to the server for SNI and is used in the client to check server - // certificates against. If ServerName is empty, the hostname used to contact the - // server is used. - ServerName string `json:"serverName,omitempty" protobuf:"bytes,2,opt,name=serverName"` - // CertData holds PEM-encoded bytes (typically read from a client certificate file). - // CertData takes precedence over CertFile - CertData []byte `json:"certData,omitempty" protobuf:"bytes,3,opt,name=certData"` - // KeyData holds PEM-encoded bytes (typically read from a client certificate key file). - // KeyData takes precedence over KeyFile - KeyData []byte `json:"keyData,omitempty" protobuf:"bytes,4,opt,name=keyData"` - // CAData holds PEM-encoded bytes (typically read from a root certificates bundle). - // CAData takes precedence over CAFile - CAData []byte `json:"caData,omitempty" protobuf:"bytes,5,opt,name=caData"` -} - -// ExecProviderConfig is config used to call an external command to perform cluster authentication -// See: https://godoc.org/k8s.io/client-go/tools/clientcmd/api#ExecConfig -type ExecProviderConfig struct { - // Command to execute - Command string `json:"command,omitempty" protobuf:"bytes,1,opt,name=command"` - - // Arguments to pass to the command when executing it - Args []string `json:"args,omitempty" protobuf:"bytes,2,rep,name=args"` - - // Env defines additional environment variables to expose to the process - Env map[string]string `json:"env,omitempty" protobuf:"bytes,3,opt,name=env"` - - // Preferred input version of the ExecInfo - APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,4,opt,name=apiVersion"` - - // This text is shown to the user when the executable doesn't seem to be present - InstallHint string `json:"installHint,omitempty" protobuf:"bytes,5,opt,name=installHint"` -} - -// RestConfig returns a go-client REST config from cluster that might be serialized into the file using kube.WriteKubeConfig method. -func (c *Cluster) RestConfig() *rest.Config { - var config *rest.Config - - tlsClientConfig := rest.TLSClientConfig{ - Insecure: c.Config.TLSClientConfig.Insecure, - ServerName: c.Config.TLSClientConfig.ServerName, - CertData: c.Config.TLSClientConfig.CertData, - KeyData: c.Config.TLSClientConfig.KeyData, - CAData: c.Config.TLSClientConfig.CAData, - } - - switch { - case c.Config.ExecProviderConfig != nil: - var env []api.ExecEnvVar - - if c.Config.ExecProviderConfig.Env != nil { - for key, value := range c.Config.ExecProviderConfig.Env { - env = append(env, api.ExecEnvVar{ - Name: key, - Value: value, - }) - } - } - - config = &rest.Config{ - Host: c.Server, - TLSClientConfig: tlsClientConfig, - ExecProvider: &api.ExecConfig{ - APIVersion: c.Config.ExecProviderConfig.APIVersion, - Command: c.Config.ExecProviderConfig.Command, - Args: c.Config.ExecProviderConfig.Args, - Env: env, - InstallHint: c.Config.ExecProviderConfig.InstallHint, - InteractiveMode: api.NeverExecInteractiveMode, - }, - } - default: - config = &rest.Config{ - Host: c.Server, - Username: c.Config.Username, - Password: c.Config.Password, - BearerToken: c.Config.BearerToken, - TLSClientConfig: tlsClientConfig, - } - } - - config.QPS = k8sClientConfigQPS - config.Burst = int(config.QPS * 2) - - return config -} diff --git a/pkg/multiclusterclient/provider.go b/pkg/multiclusterclient/provider.go index 7552387..327c969 100644 --- a/pkg/multiclusterclient/provider.go +++ b/pkg/multiclusterclient/provider.go @@ -2,22 +2,16 @@ package multiclusterclient import ( "context" - "encoding/json" "fmt" - "strings" corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" "sigs.k8s.io/controller-runtime/pkg/client" cdPipeApi "github.com/epam/edp-cd-pipeline-operator/v2/api/v1" ) -const ( - // nolint:gosec // it is not hardcoded credentials. - argocdClusterSecretLabel = "argocd.argoproj.io/secret-type" - argocdClusterSecretLabelVal = "cluster" -) - type ClientProvider struct { internalClusterClient client.Client } @@ -37,13 +31,11 @@ func (c *ClientProvider) GetClusterClient(ctx context.Context, secretNamespace, return nil, err } - cluster, err := secretToCluster(secret) + restConfig, err := secretToRestConfig(secret) if err != nil { return nil, err } - restConfig := cluster.RestConfig() - if options.Scheme == nil { options.Scheme = c.internalClusterClient.Scheme() } @@ -57,39 +49,35 @@ func (c *ClientProvider) GetClusterClient(ctx context.Context, secretNamespace, } func (c *ClientProvider) getClusterSecret(ctx context.Context, clusterName, secretNamespace string) (*corev1.Secret, error) { - secretList := &corev1.SecretList{} - if err := c.internalClusterClient.List( + secret := &corev1.Secret{} + if err := c.internalClusterClient.Get( ctx, - secretList, - client.InNamespace(secretNamespace), - client.MatchingLabels(map[string]string{argocdClusterSecretLabel: argocdClusterSecretLabelVal}), + client.ObjectKey{ + Namespace: secretNamespace, + Name: clusterName, + }, + secret, ); err != nil { - return nil, fmt.Errorf("failed to get cluster secret %s: %w", clusterName, err) - } - - for i := 0; i < len(secretList.Items); i++ { - if string(secretList.Items[i].Data["name"]) == clusterName { - return &secretList.Items[i], nil - } + return nil, fmt.Errorf("failed to get cluster secret: %w", err) } - return nil, fmt.Errorf("secret for %s cluster not found", clusterName) + return secret, nil } -func secretToCluster(s *corev1.Secret) (*Cluster, error) { - var config ClusterConfig - if len(s.Data["config"]) > 0 { - err := json.Unmarshal(s.Data["config"], &config) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal cluster config: %w", err) - } +const k8sClientConfigQPS = 50 + +func secretToRestConfig(s *corev1.Secret) (*rest.Config, error) { + if _, ok := s.Data["config"]; !ok { + return nil, fmt.Errorf("no config data in the secret %s", s.Name) } - cluster := Cluster{ - Server: strings.TrimRight(string(s.Data["server"]), "/"), - Name: string(s.Data["name"]), - Config: config, + config, err := clientcmd.RESTConfigFromKubeConfig(s.Data["config"]) + if err != nil { + return nil, fmt.Errorf("failed to create rest config from cluster secret: %w", err) } - return &cluster, nil + config.QPS = k8sClientConfigQPS + config.Burst = int(config.QPS * 2) + + return config, nil } diff --git a/pkg/multiclusterclient/provider_test.go b/pkg/multiclusterclient/provider_test.go index 848186e..b1f1ca9 100644 --- a/pkg/multiclusterclient/provider_test.go +++ b/pkg/multiclusterclient/provider_test.go @@ -27,11 +27,10 @@ func TestClientProvider_GetClusterClient(t *testing.T) { wantErr require.ErrorAssertionFunc }{ { - name: "should return internal cluster client", + name: "should return external cluster client", clusterName: "external-cluster", internalClusterClient: func(t *testing.T) client.Client { s := runtime.NewScheme() - require.NoError(t, cdPipeApi.AddToScheme(s)) require.NoError(t, corev1.AddToScheme(s)) return fake.NewClientBuilder(). @@ -39,14 +38,43 @@ func TestClientProvider_GetClusterClient(t *testing.T) { WithObjects( &corev1.Secret{ ObjectMeta: metaV1.ObjectMeta{ - Name: "secret", + Name: "external-cluster", Namespace: "default", - Labels: map[string]string{argocdClusterSecretLabel: argocdClusterSecretLabelVal}, }, Data: map[string][]byte{ - "config": []byte(`{"bearerToken": "token"}`), - "name": []byte("external-cluster"), - "server": []byte("https://external-cluster"), + "config": []byte(`{ + "apiVersion": "v1", + "kind": "Config", + "current-context": "default-context", + "preferences": {}, + "clusters": [ + { + "cluster": { + "server": "https://test-cluster", + "certificate-authority-data": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNVVENDQWZ1Z0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRUUZBREJYTVFzd0NRWURWUVFHRXdKRFRqRUwKTUFrR0ExVUVDQk1DVUU0eEN6QUpCZ05WQkFjVEFrTk9NUXN3Q1FZRFZRUUtFd0pQVGpFTE1Ba0dBMVVFQ3hNQwpWVTR4RkRBU0JnTlZCQU1UQzBobGNtOXVaeUJaWVc1bk1CNFhEVEExTURjeE5USXhNVGswTjFvWERUQTFNRGd4Ck5ESXhNVGswTjFvd1Z6RUxNQWtHQTFVRUJoTUNRMDR4Q3pBSkJnTlZCQWdUQWxCT01Rc3dDUVlEVlFRSEV3SkQKVGpFTE1Ba0dBMVVFQ2hNQ1QwNHhDekFKQmdOVkJBc1RBbFZPTVJRd0VnWURWUVFERXd0SVpYSnZibWNnV1dGdQpaekJjTUEwR0NTcUdTSWIzRFFFQkFRVUFBMHNBTUVnQ1FRQ3A1aG5HN29nQmh0bHlucE9TMjFjQmV3S0UvQjdqClYxNHFleXNsbnIyNnhaVXNTVmtvMzZabmhpYU8vemJNT29SY0tLOXZFY2dNdGNMRnVRVFdEbDNSQWdNQkFBR2oKZ2JFd2dhNHdIUVlEVlIwT0JCWUVGRlhJNzBrclhlUUR4WmdiYUNRb1I0alVEbmNFTUg4R0ExVWRJd1I0TUhhQQpGRlhJNzBrclhlUUR4WmdiYUNRb1I0alVEbmNFb1Z1a1dUQlhNUXN3Q1FZRFZRUUdFd0pEVGpFTE1Ba0dBMVVFCkNCTUNVRTR4Q3pBSkJnTlZCQWNUQWtOT01Rc3dDUVlEVlFRS0V3SlBUakVMTUFrR0ExVUVDeE1DVlU0eEZEQVMKQmdOVkJBTVRDMGhsY205dVp5QlpZVzVuZ2dFQU1Bd0dBMVVkRXdRRk1BTUJBZjh3RFFZSktvWklodmNOQVFFRQpCUUFEUVFBL3VnekJyampLOWpjV25EVmZHSGxrM2ljTlJxMG9WN1JpMzJ6LytIUVg2N2FSZmdadTdLV2RJK0p1CldtN0RDZnJQTkdWd0ZXVVFPbXNQdWU5clpCZ08KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=" + }, + "name": "default-cluster" + } + ], + "contexts": [ + { + "context": { + "cluster": "default-cluster", + "user": "default-user" + }, + "name": "default-context" + } + ], + "users": [ + { + "user": { + "token": "token-123" + }, + "name": "default-user" + } + ] + }`, + ), }, }, ). @@ -60,7 +88,6 @@ func TestClientProvider_GetClusterClient(t *testing.T) { clusterName: "external-cluster", internalClusterClient: func(t *testing.T) client.Client { s := runtime.NewScheme() - require.NoError(t, cdPipeApi.AddToScheme(s)) require.NoError(t, corev1.AddToScheme(s)) return fake.NewClientBuilder(). @@ -68,13 +95,11 @@ func TestClientProvider_GetClusterClient(t *testing.T) { WithObjects( &corev1.Secret{ ObjectMeta: metaV1.ObjectMeta{ - Name: "secret", + Name: "external-cluster", Namespace: "default", - Labels: map[string]string{argocdClusterSecretLabel: argocdClusterSecretLabelVal}, }, Data: map[string][]byte{ "config": []byte(`not json data`), - "name": []byte("external-cluster"), }, }, ). @@ -83,7 +108,33 @@ func TestClientProvider_GetClusterClient(t *testing.T) { want: require.Nil, wantErr: func(t require.TestingT, err error, i ...interface{}) { require.Error(t, err) - require.Contains(t, err.Error(), "failed to unmarshal cluster config") + require.Contains(t, err.Error(), "failed to create rest config from cluster secret") + }, + }, + { + name: "secret does not contain config data", + clusterName: "external-cluster", + internalClusterClient: func(t *testing.T) client.Client { + s := runtime.NewScheme() + require.NoError(t, corev1.AddToScheme(s)) + + return fake.NewClientBuilder(). + WithScheme(s). + WithObjects( + &corev1.Secret{ + ObjectMeta: metaV1.ObjectMeta{ + Name: "external-cluster", + Namespace: "default", + }, + Data: map[string][]byte{}, + }, + ). + Build() + }, + want: require.Nil, + wantErr: func(t require.TestingT, err error, i ...interface{}) { + require.Error(t, err) + require.Contains(t, err.Error(), "no config data in the secret") }, }, { @@ -91,7 +142,6 @@ func TestClientProvider_GetClusterClient(t *testing.T) { clusterName: "external-cluster", internalClusterClient: func(t *testing.T) client.Client { s := runtime.NewScheme() - require.NoError(t, cdPipeApi.AddToScheme(s)) require.NoError(t, corev1.AddToScheme(s)) return fake.NewClientBuilder(). @@ -110,7 +160,6 @@ func TestClientProvider_GetClusterClient(t *testing.T) { clusterName: cdPipeApi.InCluster, internalClusterClient: func(t *testing.T) client.Client { s := runtime.NewScheme() - require.NoError(t, cdPipeApi.AddToScheme(s)) require.NoError(t, corev1.AddToScheme(s)) return fake.NewClientBuilder().