func/pkg/k8s/secrets.go

180 lines
5.5 KiB
Go

package k8s
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"net"
"syscall"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sclientcmd "k8s.io/client-go/tools/clientcmd"
)
func GetSecret(ctx context.Context, name, namespaceOverride string) (*corev1.Secret, error) {
client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride)
if err != nil {
return nil, err
}
return client.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{})
}
// ListSecretsNamesIfConnected lists names of Secrets present and the current k8s context
// returns empty list, if not connected to any cluster
func ListSecretsNamesIfConnected(ctx context.Context, namespaceOverride string) (names []string, err error) {
names, err = listSecretsNames(ctx, namespaceOverride)
if err != nil {
// not logged our authorized to access resources
if k8serrors.IsForbidden(err) || k8serrors.IsUnauthorized(err) || k8serrors.IsInvalid(err) || k8serrors.IsTimeout(err) {
return []string{}, nil
}
// non existent k8s cluster
var dnsErr *net.DNSError
if errors.As(err, &dnsErr) {
if dnsErr.IsNotFound || dnsErr.IsTemporary || dnsErr.IsTimeout {
return []string{}, nil
}
}
// connection refused
if errors.Is(err, syscall.ECONNREFUSED) {
return []string{}, nil
}
// invalid configuration: no configuration has been provided
if k8sclientcmd.IsEmptyConfig(err) {
return []string{}, nil
}
}
return
}
func listSecretsNames(ctx context.Context, namespaceOverride string) (names []string, err error) {
client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride)
if err != nil {
return
}
secrets, err := client.CoreV1().Secrets(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
return
}
for _, s := range secrets.Items {
names = append(names, s.Name)
}
return
}
func DeleteSecrets(ctx context.Context, namespaceOverride string, listOptions metav1.ListOptions) (err error) {
client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride)
if err != nil {
return
}
return client.CoreV1().Secrets(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, listOptions)
}
func EnsureDockerRegistrySecretExist(ctx context.Context, name, namespaceOverride string, labels map[string]string, annotations map[string]string, username, password, server string) (err error) {
dockerConfigJSONContent, err := HandleDockerCfgJSONContent(username, password, "", server)
if err != nil {
return
}
// Check whether we need to create or update the Secret
secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Labels: labels,
Annotations: annotations,
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{},
}
secret.Data["config.json"] = dockerConfigJSONContent
return EnsureSecretExist(ctx, secret, namespaceOverride)
}
func EnsureSecretExist(ctx context.Context, secret corev1.Secret, namespaceOverride string) (err error) {
client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride)
if err != nil {
return
}
// Check whether Secret with specified name exist
secretNotFound := false
existingSecret, err := GetSecret(ctx, secret.Name, namespace)
if err != nil {
if !k8serrors.IsNotFound(err) {
return
}
secretNotFound = true
}
// TODO we should also compare labels and annotations
if secretNotFound || !equality.Semantic.DeepDerivative(existingSecret.Data, secret.Data) {
// Decide whether create or update
if secretNotFound {
_, err = client.CoreV1().Secrets(namespace).Create(ctx, &secret, metav1.CreateOptions{})
} else {
_, err = client.CoreV1().Secrets(namespace).Update(ctx, &secret, metav1.UpdateOptions{})
}
}
return
}
// --- Helper methods for DockerConfigJson type of Secret
// Taken from (and converted to private):
// https://github.com/kubernetes/kubectl/blob/10c4667470db41ce138b9aae4e9590dbd7f1930d/pkg/cmd/create/create_secret_docker.go#L290
// DockerConfigJSON represents a local docker auth config file
// for pulling images.
type dockerConfigJSON struct {
Auths dockerConfig `json:"auths" datapolicy:"token"`
// +optional
HttpHeaders map[string]string `json:"HttpHeaders,omitempty" datapolicy:"token"`
}
// DockerConfig represents the config file used by the docker CLI.
// This config that represents the credentials that should be used
// when pulling images from specific image repositories.
type dockerConfig map[string]dockerConfigEntry
// dockerConfigEntry holds the user information that grant the access to docker registry
type dockerConfigEntry struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty" datapolicy:"password"`
Email string `json:"email,omitempty"`
Auth string `json:"auth,omitempty" datapolicy:"token"`
}
func HandleDockerCfgJSONContent(username, password, email, server string) ([]byte, error) {
dockerConfigAuth := dockerConfigEntry{
Username: username,
Password: password,
Email: email,
Auth: encodeDockerConfigFieldAuth(username, password),
}
dockerConfigJSON := dockerConfigJSON{
Auths: map[string]dockerConfigEntry{server: dockerConfigAuth},
}
return json.Marshal(dockerConfigJSON)
}
// encodeDockerConfigFieldAuth returns base64 encoding of the username and password string
func encodeDockerConfigFieldAuth(username, password string) string {
fieldValue := username + ":" + password
return base64.StdEncoding.EncodeToString([]byte(fieldValue))
}