github.com/kubeshop/testkube@v1.17.23/pkg/skopeo/client.go (about)

     1  package skopeo
     2  
     3  import (
     4  	"encoding/base64"
     5  	"encoding/json"
     6  	"fmt"
     7  	"math/rand"
     8  	"regexp"
     9  	"strings"
    10  	"time"
    11  
    12  	corev1 "k8s.io/api/core/v1"
    13  
    14  	"github.com/kubeshop/testkube/pkg/process"
    15  )
    16  
    17  // DockerAuths contains an embedded DockerAuthConfigs
    18  type DockerAuths struct {
    19  	Auths DockerAuthConfigs `json:"auths"`
    20  }
    21  
    22  // DockerAuthConfigs is a map of registries and their credentials
    23  type DockerAuthConfigs map[string]DockerAuthConfig
    24  
    25  // DockerAuthConfig contains authorization information for connecting to a registry
    26  // It mirrors "github.com/docker/docker/api/types.AuthConfig"
    27  type DockerAuthConfig struct {
    28  	Username string `json:"username,omitempty"`
    29  	Password string `json:"password,omitempty"`
    30  	Auth     string `json:"auth,omitempty"`
    31  
    32  	// Email is an optional value associated with the username.
    33  	// This field is deprecated and will be removed in a later
    34  	// version of docker.
    35  	Email string `json:"email,omitempty"`
    36  }
    37  
    38  // DockerImage contains definition of docker image
    39  type DockerImage struct {
    40  	Config struct {
    41  		User       string   `json:"User"`
    42  		Entrypoint []string `json:"Entrypoint"`
    43  		Cmd        []string `json:"Cmd"`
    44  		WorkingDir string   `json:"WorkingDir"`
    45  	} `json:"config"`
    46  	History []struct {
    47  		Created   time.Time `json:"created"`
    48  		CreatedBy string    `json:"created_by"`
    49  	} `json:"history"`
    50  	Shell string `json:"-"`
    51  }
    52  
    53  // Inspector is image inspector interface
    54  type Inspector interface {
    55  	Inspect(registry, image string) (*DockerImage, error)
    56  }
    57  
    58  type client struct {
    59  	dockerAuthConfigs []DockerAuthConfig
    60  }
    61  
    62  var _ Inspector = (*client)(nil)
    63  
    64  // NewClient creates new empty client
    65  func NewClient() *client {
    66  	return &client{}
    67  }
    68  
    69  // NewClientFromSecrets creats new client from secrets
    70  func NewClientFromSecrets(imageSecrets []corev1.Secret, registry string) (*client, error) {
    71  	auths, err := ParseSecretData(imageSecrets, registry)
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  
    76  	return &client{dockerAuthConfigs: auths}, nil
    77  }
    78  
    79  // Inspect inspect a docker image
    80  func (c *client) Inspect(registry, image string) (*DockerImage, error) {
    81  	args := []string{
    82  		"--override-os",
    83  		"linux",
    84  		"inspect",
    85  	}
    86  
    87  	if len(c.dockerAuthConfigs) != 0 {
    88  		i := rand.Intn(len(c.dockerAuthConfigs))
    89  		args = append(args, "--creds", c.dockerAuthConfigs[i].Username+":"+c.dockerAuthConfigs[i].Password)
    90  	}
    91  
    92  	config := "docker://" + image
    93  	if registry != "" {
    94  		config = registry + "/" + image
    95  	}
    96  
    97  	args = append(args, "--config", config)
    98  	result, err := process.Execute("skopeo", args...)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  
   103  	var dockerImage DockerImage
   104  	if err = json.Unmarshal(result, &dockerImage); err != nil {
   105  		return nil, err
   106  	}
   107  
   108  	var shell string
   109  	for i := len(dockerImage.History); i > 0; i-- {
   110  		command := dockerImage.History[i-1].CreatedBy
   111  		re, err := regexp.Compile(`/bin/([a-z]*)sh`)
   112  		if err != nil {
   113  			return nil, err
   114  		}
   115  
   116  		shell = re.FindString(command)
   117  		if shell != "" {
   118  			break
   119  		}
   120  	}
   121  
   122  	dockerImage.Shell = shell
   123  	return &dockerImage, nil
   124  }
   125  
   126  // ParseSecretData parses secret data for docker auth config
   127  func ParseSecretData(imageSecrets []corev1.Secret, registry string) ([]DockerAuthConfig, error) {
   128  	var results []DockerAuthConfig
   129  	for _, imageSecret := range imageSecrets {
   130  		auths := DockerAuths{}
   131  		if jsonData, ok := imageSecret.Data[".dockerconfigjson"]; ok {
   132  			if err := json.Unmarshal(jsonData, &auths); err != nil {
   133  				return nil, err
   134  			}
   135  		} else if configData, ok := imageSecret.Data[".dockercfg"]; ok {
   136  			if err := json.Unmarshal(configData, &auths.Auths); err != nil {
   137  				return nil, err
   138  			}
   139  		} else {
   140  			return nil, fmt.Errorf("imagePullSecret %s contains neither .dockercfg nor .dockerconfigjson", imageSecret.Name)
   141  		}
   142  
   143  		// If registry is not provided, extract it from the image name
   144  		if registry == "" {
   145  			registry = extractRegistry(imageSecret.Name)
   146  		}
   147  		// Determine if there is a secret for the specified registry
   148  		if creds, ok := auths.Auths[registry]; ok {
   149  			username, password, err := extractRegistryCredentials(creds)
   150  			if err != nil {
   151  				return nil, err
   152  			}
   153  
   154  			results = append(results, DockerAuthConfig{Username: username, Password: password})
   155  		} else {
   156  			return nil, fmt.Errorf("secret %s is not defined for registry: %s", imageSecret.Name, registry)
   157  		}
   158  	}
   159  
   160  	return results, nil
   161  }
   162  
   163  // extractRegistry takes a container image string and returns the registry part.
   164  // It defaults to "docker.io" if no registry is specified.
   165  func extractRegistry(image string) string {
   166  	defaultRegistry := "https://index.docker.io/v1/"
   167  	parts := strings.Split(image, "/")
   168  	// If the image is just a name, return the default registry.
   169  	if len(parts) == 1 {
   170  		return defaultRegistry
   171  	}
   172  	// If the first part contains '.' or ':', it's likely a registry.
   173  	if strings.Contains(parts[0], ".") || strings.Contains(parts[0], ":") {
   174  		return parts[0]
   175  	}
   176  	return defaultRegistry
   177  }
   178  
   179  func extractRegistryCredentials(creds DockerAuthConfig) (username, password string, err error) {
   180  	if creds.Auth == "" {
   181  		return creds.Username, creds.Password, nil
   182  	}
   183  
   184  	decoder := base64.StdEncoding
   185  	if !strings.HasSuffix(strings.TrimSpace(creds.Auth), "=") {
   186  		// Modify the decoder to be raw if no padding is present
   187  		decoder = decoder.WithPadding(base64.NoPadding)
   188  	}
   189  
   190  	base64Decoded, err := decoder.DecodeString(creds.Auth)
   191  	if err != nil {
   192  		return "", "", err
   193  	}
   194  
   195  	splitted := strings.SplitN(string(base64Decoded), ":", 2)
   196  	if len(splitted) != 2 {
   197  		return creds.Username, creds.Password, nil
   198  	}
   199  
   200  	return splitted[0], splitted[1], nil
   201  }