Skip to content

Commit d94104f

Browse files
committed
feat(kube-run): --auto-pull-secret provides private registry access for pod
Signed-off-by: Ilya Lesikov <ilya@lesikov.com>
1 parent 352a0bd commit d94104f

File tree

1 file changed

+187
-16
lines changed

1 file changed

+187
-16
lines changed

cmd/werf/kube_run/kube_run.go

Lines changed: 187 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ package kube_run
22

33
import (
44
"context"
5+
"encoding/base64"
6+
"encoding/json"
57
"fmt"
68
"math/rand"
79
"os"
810
"os/exec"
911
"strings"
1012

13+
"github.com/containers/image/v5/docker/reference"
14+
config2 "github.com/containers/image/v5/pkg/docker/config"
15+
imgtypes "github.com/containers/image/v5/types"
1116
"github.com/spf13/cobra"
1217
corev1 "k8s.io/api/core/v1"
1318
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -40,6 +45,7 @@ type cmdDataType struct {
4045
AllocateTty bool
4146
Rm bool
4247
RmWithNamespace bool
48+
AutoPullSecret bool
4349

4450
Pod string
4551
Command []string
@@ -53,6 +59,15 @@ var (
5359
commonCmdData common.CmdData
5460
)
5561

62+
type dockerConfigJson struct {
63+
Auths map[string]dockerAuthJson `json:"auths"`
64+
}
65+
66+
type dockerAuthJson struct {
67+
Auth string `json:"auth,omitempty"`
68+
IdentityToken string `json:"identitytoken,omitempty"`
69+
}
70+
5671
func NewCmd() *cobra.Command {
5772
cmd := &cobra.Command{
5873
Use: "kube-run [options] [IMAGE_NAME] [-- COMMAND ARG...]",
@@ -153,10 +168,11 @@ func NewCmd() *cobra.Command {
153168
cmd.Flags().StringVarP(&cmdData.Pod, "pod", "", os.Getenv("WERF_POD"), "Set created pod name (default $WERF_POD or autogenerated if not specified)")
154169
cmd.Flags().StringVarP(&cmdData.Overrides, "overrides", "", os.Getenv("WERF_OVERRIDES"), "Inline JSON to override/extend any fields in created Pod, e.g. to add imagePullSecrets field (default $WERF_OVERRIDES)")
155170
cmd.Flags().StringVarP(&cmdData.ExtraOptions, "extra-options", "", os.Getenv("WERF_EXTRA_OPTIONS"), "Pass extra options to \"kubectl run\" command (default $WERF_EXTRA_OPTIONS)")
156-
cmd.Flags().BoolVarP(&cmdData.Rm, "rm", "", common.GetBoolEnvironmentDefaultTrue("WERF_RM"), "Remove pod after command completion (default $WERF_RM or true if not specified)")
171+
cmd.Flags().BoolVarP(&cmdData.Rm, "rm", "", common.GetBoolEnvironmentDefaultTrue("WERF_RM"), "Remove pod and other created resources after command completion (default $WERF_RM or true if not specified)")
157172
cmd.Flags().BoolVarP(&cmdData.RmWithNamespace, "rm-with-namespace", "", common.GetBoolEnvironmentDefaultFalse("WERF_RM_WITH_NAMESPACE"), "Remove also a namespace after command completion (default $WERF_RM_WITH_NAMESPACE or false if not specified)")
158173
cmd.Flags().BoolVarP(&cmdData.Interactive, "interactive", "i", common.GetBoolEnvironmentDefaultFalse("WERF_INTERACTIVE"), "Enable interactive mode (default $WERF_INTERACTIVE or false if not specified)")
159174
cmd.Flags().BoolVarP(&cmdData.AllocateTty, "tty", "t", common.GetBoolEnvironmentDefaultFalse("WERF_TTY"), "Allocate a TTY (default $WERF_TTY or false if not specified)")
175+
cmd.Flags().BoolVarP(&cmdData.AutoPullSecret, "auto-pull-secret", "", common.GetBoolEnvironmentDefaultTrue("WERF_AUTO_PULL_SECRET"), "Automatically create docker config secret in the namespace and plug it via pod's imagePullSecrets for private registry access (default $WERF_AUTO_PULL_SECRET or true if not specified)")
160176

161177
return cmd
162178
}
@@ -252,6 +268,7 @@ func runMain(ctx context.Context) error {
252268
} else {
253269
pod = cmdData.Pod
254270
}
271+
secret := pod
255272

256273
_, werfConfig, err := common.GetRequiredWerfConfig(ctx, &commonCmdData, giterminismManager, common.GetWerfConfigOptions(&commonCmdData, false))
257274
if err != nil {
@@ -271,31 +288,31 @@ func runMain(ctx context.Context) error {
271288
}
272289

273290
defer func() {
274-
cleanupResources(ctx, pod, namespace)
291+
cleanupResources(ctx, pod, secret, namespace)
275292
}()
276293

277294
if *commonCmdData.Follow {
278295
return common.FollowGitHead(ctx, &commonCmdData, func(ctx context.Context, headCommitGiterminismManager giterminism_manager.Interface) error {
279-
cleanupResources(ctx, pod, namespace)
296+
cleanupResources(ctx, pod, secret, namespace)
280297

281-
if err := run(ctx, pod, namespace, werfConfig, containerRuntime, giterminismManager); err != nil {
298+
if err := run(ctx, pod, secret, namespace, werfConfig, containerRuntime, giterminismManager); err != nil {
282299
return err
283300
}
284301

285-
cleanupResources(ctx, pod, namespace)
302+
cleanupResources(ctx, pod, secret, namespace)
286303

287304
return nil
288305
})
289306
} else {
290-
if err := run(ctx, pod, namespace, werfConfig, containerRuntime, giterminismManager); err != nil {
307+
if err := run(ctx, pod, secret, namespace, werfConfig, containerRuntime, giterminismManager); err != nil {
291308
return err
292309
}
293310
}
294311

295312
return nil
296313
}
297314

298-
func run(ctx context.Context, pod string, namespace string, werfConfig *config.WerfConfig, containerRuntime container_runtime.ContainerRuntime, giterminismManager giterminism_manager.Interface) error {
315+
func run(ctx context.Context, pod, secret, namespace string, werfConfig *config.WerfConfig, containerRuntime container_runtime.ContainerRuntime, giterminismManager giterminism_manager.Interface) error {
299316
projectName := werfConfig.Meta.Project
300317

301318
projectTmpDir, err := tmp_manager.CreateProjectDir(ctx)
@@ -371,6 +388,19 @@ func run(ctx context.Context, pod string, namespace string, werfConfig *config.W
371388
return err
372389
}
373390

391+
if cmdData.AutoPullSecret {
392+
namedRef, dockerAuthConf, err := getDockerConfigCredentials(image)
393+
if err != nil {
394+
return fmt.Errorf("unable to get docker config credentials: %w", err)
395+
}
396+
397+
if dockerAuthConf != (imgtypes.DockerAuthConfig{}) {
398+
if err := createDockerRegistrySecret(ctx, secret, namespace, *namedRef, dockerAuthConf); err != nil {
399+
return fmt.Errorf("unable to create docker registry secret: %w", err)
400+
}
401+
}
402+
}
403+
374404
args := []string{
375405
"kubectl",
376406
"run",
@@ -391,8 +421,26 @@ func run(ctx context.Context, pod string, namespace string, werfConfig *config.W
391421
args = append(args, "-t")
392422
}
393423

424+
var overrides map[string]interface{}
394425
if cmdData.Overrides != "" {
395-
args = append(args, "--overrides", cmdData.Overrides)
426+
if err := json.Unmarshal([]byte(cmdData.Overrides), &overrides); err != nil {
427+
return fmt.Errorf("unable to unmarshal --overrides: %w", err)
428+
}
429+
}
430+
431+
if cmdData.AutoPullSecret {
432+
overrides, err = addImagePullSecret(secret, overrides)
433+
if err != nil {
434+
return fmt.Errorf("unable to add imagePullSecret to --overrides: %w", err)
435+
}
436+
}
437+
438+
if len(overrides) > 0 {
439+
overridesB, err := json.Marshal(overrides)
440+
if err != nil {
441+
return fmt.Errorf("unable to marshal generated --overrides: %w", err)
442+
}
443+
args = append(args, "--overrides", string(overridesB))
396444
}
397445

398446
if cmdData.ExtraOptions != "" {
@@ -431,21 +479,34 @@ func run(ctx context.Context, pod string, namespace string, werfConfig *config.W
431479
})
432480
}
433481

434-
func cleanupResources(ctx context.Context, pod string, namespace string) {
482+
func cleanupResources(ctx context.Context, pod, secret, namespace string) {
483+
if !cmdData.Rm {
484+
return
485+
}
486+
435487
if isNsExist, err := isNamespaceExist(ctx, namespace); err != nil {
436488
logboek.Context(ctx).Warn().LogF("WARNING: unable to check for namespace existence: %s\n", err)
437489
return
438490
} else if !isNsExist {
439491
return
440492
}
441493

442-
if cmdData.Rm {
443-
if isPodExist, err := isPodExist(ctx, pod, namespace); err != nil {
444-
logboek.Context(ctx).Warn().LogF("WARNING: unable to check for pod existence: %s\n", err)
445-
} else if isPodExist {
446-
logboek.Context(ctx).LogF("Cleaning up pod %q ...\n", pod)
447-
if err := kube.Client.CoreV1().Pods(namespace).Delete(ctx, pod, v1.DeleteOptions{}); err != nil {
448-
logboek.Context(ctx).Warn().LogF("WARNING: pod cleaning up failed: %s\n", err)
494+
if isPodExist, err := isPodExist(ctx, pod, namespace); err != nil {
495+
logboek.Context(ctx).Warn().LogF("WARNING: unable to check for pod existence: %s\n", err)
496+
} else if isPodExist {
497+
logboek.Context(ctx).LogF("Cleaning up pod %q ...\n", pod)
498+
if err := kube.Client.CoreV1().Pods(namespace).Delete(ctx, pod, v1.DeleteOptions{}); err != nil {
499+
logboek.Context(ctx).Warn().LogF("WARNING: pod cleaning up failed: %s\n", err)
500+
}
501+
}
502+
503+
if cmdData.AutoPullSecret {
504+
if isSecretExist, err := isSecretExist(ctx, secret, namespace); err != nil {
505+
logboek.Context(ctx).Warn().LogF("WARNING: unable to check for secret existence: %s\n", err)
506+
} else if isSecretExist {
507+
logboek.Context(ctx).LogF("Cleaning up secret %q ...\n", secret)
508+
if err := kube.Client.CoreV1().Secrets(namespace).Delete(ctx, secret, v1.DeleteOptions{}); err != nil {
509+
logboek.Context(ctx).Warn().LogF("WARNING: secret cleaning up failed: %s\n", err)
449510
}
450511
}
451512
}
@@ -480,6 +541,45 @@ func createNamespace(ctx context.Context, namespace string) error {
480541
return nil
481542
}
482543

544+
func createDockerRegistrySecret(ctx context.Context, name, namespace string, ref reference.Named, dockerAuthConf imgtypes.DockerAuthConfig) error {
545+
secret := &corev1.Secret{
546+
ObjectMeta: v1.ObjectMeta{
547+
Name: name,
548+
Namespace: namespace,
549+
},
550+
Data: map[string][]byte{},
551+
Type: corev1.SecretTypeDockerConfigJson,
552+
}
553+
554+
var authJson dockerAuthJson
555+
switch {
556+
case dockerAuthConf.IdentityToken != "":
557+
authJson.IdentityToken = dockerAuthConf.IdentityToken
558+
case dockerAuthConf.Username != "" && dockerAuthConf.Password != "":
559+
authJson.Auth = base64.StdEncoding.EncodeToString([]byte(dockerAuthConf.Username + ":" + dockerAuthConf.Password))
560+
default:
561+
panic("unexpected dockerAuthConf")
562+
}
563+
564+
dockerConfJson := &dockerConfigJson{
565+
Auths: map[string]dockerAuthJson{
566+
ref.Name(): authJson,
567+
},
568+
}
569+
570+
dockerConf, err := json.Marshal(dockerConfJson)
571+
if err != nil {
572+
return fmt.Errorf("unable to marshal docker config json: %w", err)
573+
}
574+
575+
secret.Data[corev1.DockerConfigJsonKey] = dockerConf
576+
577+
logboek.Context(ctx).LogF("Creating secret %q in namespace %q ...\n", name, namespace)
578+
kube.Client.CoreV1().Secrets(namespace).Create(ctx, secret, v1.CreateOptions{})
579+
580+
return nil
581+
}
582+
483583
func isNamespaceExist(ctx context.Context, namespace string) (bool, error) {
484584
if matchedNamespaces, err := kube.Client.CoreV1().Namespaces().List(ctx, v1.ListOptions{
485585
FieldSelector: fields.OneTermEqualSelector("metadata.name", namespace).String(),
@@ -503,3 +603,74 @@ func isPodExist(ctx context.Context, pod string, namespace string) (bool, error)
503603

504604
return false, nil
505605
}
606+
607+
func isSecretExist(ctx context.Context, secret string, namespace string) (bool, error) {
608+
if matchedSecrets, err := kube.Client.CoreV1().Secrets(namespace).List(ctx, v1.ListOptions{
609+
FieldSelector: fields.OneTermEqualSelector("metadata.name", secret).String(),
610+
}); err != nil {
611+
return false, fmt.Errorf("unable to list secrets: %w", err)
612+
} else if len(matchedSecrets.Items) > 0 {
613+
return true, nil
614+
}
615+
616+
return false, nil
617+
}
618+
619+
// Might return empty DockerAuthConfig.
620+
func getDockerConfigCredentials(ref string) (*reference.Named, imgtypes.DockerAuthConfig, error) {
621+
namedRef, err := reference.ParseNormalizedNamed(ref)
622+
if err != nil {
623+
return nil, imgtypes.DockerAuthConfig{}, fmt.Errorf("unable to parse docker config registry reference %q: %w", ref, err)
624+
}
625+
626+
dockerAuthConf, err := config2.GetCredentialsForRef(&imgtypes.SystemContext{AuthFilePath: *commonCmdData.DockerConfig}, namedRef)
627+
if err != nil {
628+
return nil, imgtypes.DockerAuthConfig{}, fmt.Errorf("unable to get docker registry creds for ref %q: %w", ref, err)
629+
}
630+
631+
return &namedRef, dockerAuthConf, nil
632+
}
633+
634+
func addImagePullSecret(secret string, overrides map[string]interface{}) (map[string]interface{}, error) {
635+
if secret == "" {
636+
panic("secret name can't be empty")
637+
}
638+
639+
newImagePullSecret := map[string]interface{}{"name": secret}
640+
newImagePullSecrets := []interface{}{newImagePullSecret}
641+
newSpec := map[string]interface{}{"imagePullSecrets": newImagePullSecrets}
642+
newOverrides := map[string]interface{}{"spec": newSpec}
643+
644+
if len(overrides) == 0 {
645+
return newOverrides, nil
646+
}
647+
648+
if _, ok := overrides["spec"]; !ok {
649+
overrides["spec"] = newSpec
650+
return overrides, nil
651+
}
652+
653+
overridesSpec, ok := overrides["spec"].(map[string]interface{})
654+
if !ok {
655+
return nil, fmt.Errorf("unexpected pod spec overrides format: %+v", overrides)
656+
}
657+
658+
if len(overridesSpec) == 0 {
659+
overrides["spec"] = newSpec
660+
return overrides, nil
661+
}
662+
663+
_, ok = overridesSpec["imagePullSecrets"]
664+
if !ok {
665+
overrides["spec"].(map[string]interface{})["imagePullSecrets"] = newImagePullSecrets
666+
return overrides, nil
667+
}
668+
669+
_, ok = overridesSpec["imagePullSecrets"].([]interface{})
670+
if !ok {
671+
return nil, fmt.Errorf("unexpected imagePullSecrets overrides format: %+v", overrides)
672+
}
673+
674+
overrides["spec"].(map[string]interface{})["imagePullSecrets"] = append(overrides["spec"].(map[string]interface{})["imagePullSecrets"].([]interface{}), newImagePullSecret)
675+
return overrides, nil
676+
}

0 commit comments

Comments
 (0)